diff --git a/package-lock.json b/package-lock.json index 092190a..bae9a23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,27 @@ { "name": "mmx-cli", - "version": "1.0.5", + "version": "1.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mmx-cli", - "version": "1.0.5", + "version": "1.0.12", "dependencies": { - "@clack/prompts": "^0.7.0", - "yaml": "^2.7.1" + "@clack/prompts": "^0.7.0" }, "bin": { "mmx": "dist/mmx.mjs" }, "devDependencies": { + "@eslint/js": "^9.0.0", "@types/bun": "latest", "eslint": "^9.24.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "typescript-eslint": "^8.58.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/@clack/core": { @@ -47,6 +51,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -286,6 +291,288 @@ "undici-types": "~7.18.0" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", + "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/type-utils": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://npmmirror.xaminim.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/parser/-/parser-8.59.2.tgz", + "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", + "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.2", + "@typescript-eslint/types": "^8.59.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", + "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", + "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", + "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/types/-/types-8.59.2.tgz", + "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", + "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.2", + "@typescript-eslint/tsconfig-utils": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://npmmirror.xaminim.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://npmmirror.xaminim.com/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://npmmirror.xaminim.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/utils/-/utils-8.59.2.tgz", + "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", + "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://npmmirror.xaminim.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", @@ -659,6 +946,24 @@ "dev": true, "license": "MIT" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://npmmirror.xaminim.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -1010,6 +1315,19 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://npmmirror.xaminim.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -1040,6 +1358,19 @@ "node": ">=4" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://npmmirror.xaminim.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1095,6 +1426,36 @@ "node": ">=8" } }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://npmmirror.xaminim.com/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://npmmirror.xaminim.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", @@ -1122,6 +1483,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.59.2", + "resolved": "https://npmmirror.xaminim.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz", + "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.2", + "@typescript-eslint/parser": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", @@ -1165,21 +1550,6 @@ "node": ">=0.10.0" } }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/src/auth/oauth.ts b/src/auth/oauth.ts index 530f03e..4dab870 100644 --- a/src/auth/oauth.ts +++ b/src/auth/oauth.ts @@ -2,9 +2,10 @@ import type { OAuthTokens } from './types'; import { CLIError } from '../errors/base'; import { ExitCode } from '../errors/codes'; -// OAuth configuration — exact endpoints TBD pending MiniMax OAuth docs +// OAuth configuration export interface OAuthConfig { clientId: string; + clientName: string; authorizationUrl: string; tokenUrl: string; deviceCodeUrl: string; @@ -12,17 +13,8 @@ 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, -}; - export async function startBrowserFlow( - config: OAuthConfig = DEFAULT_OAUTH_CONFIG, + config: OAuthConfig, ): Promise { const { randomBytes, createHash } = await import('crypto'); const codeVerifier = randomBytes(32).toString('base64url'); @@ -137,68 +129,112 @@ async function waitForCallback(port: number, expectedState: string): Promise { - // Request device code + const { randomBytes, createHash } = await import('crypto'); + const codeVerifier = randomBytes(32).toString('base64url'); + const codeChallenge = createHash('sha256') + .update(codeVerifier) + .digest('base64url'); + + const state = randomBytes(16).toString('base64url'); + + const lane = process.env.BEDROCK_LANE; + const extraHeaders: Record = lane ? { bedrock_lane: lane } : {}; + if (process.env.X_USER_PRE) extraHeaders['X-User-Pre'] = 'true'; + + // Request device code with PKCE const codeRes = await fetch(config.deviceCodeUrl, { method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...extraHeaders }, body: new URLSearchParams({ client_id: config.clientId, scope: config.scopes.join(' '), + code_challenge: codeChallenge, + code_challenge_method: 'S256', + state, }), }); if (!codeRes.ok) { + const body = await codeRes.text().catch(() => ''); throw new CLIError( - 'Failed to start device code flow.', + `Failed to start device code flow: HTTP ${codeRes.status} ${body}`, ExitCode.AUTH, + `URL: ${config.deviceCodeUrl}`, ); } - const { device_code, user_code, verification_uri, interval, expires_in } = - (await codeRes.json()) as { - device_code: string; - user_code: string; - verification_uri: string; - interval: number; - expires_in: number; - }; + const data = (await codeRes.json()) as { + user_code: string; + verification_uri: string; + expired_in: number; // Unix timestamp (ms) + interval: number; // milliseconds + state: string; + }; + + if (data.state !== state) { + throw new CLIError('OAuth state mismatch: possible CSRF attack.', ExitCode.AUTH); + } + + const url = data.verification_uri; - process.stderr.write(`\nVisit: ${verification_uri}\n`); - process.stderr.write(`Enter code: ${user_code}\n`); + const { exec } = await import('child_process'); + const openCmd = process.platform === 'darwin' ? 'open' : + process.platform === 'win32' ? 'start' : 'xdg-open'; + exec(`${openCmd} "${url}"`); + + process.stderr.write(`\nOpened: ${url}\n`); + process.stderr.write(`Enter code: ${data.user_code}\n`); process.stderr.write('Waiting for authorization...\n'); - // Poll for authorization - const deadline = Date.now() + expires_in * 1000; - const pollInterval = (interval || 5) * 1000; + // Poll for authorization (expired_in is Unix timestamp in ms) + const deadline = data.expired_in; + const pollInterval = data.interval || 5000; while (Date.now() < deadline) { await new Promise(r => setTimeout(r, pollInterval)); const tokenRes = await fetch(config.tokenUrl, { method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...extraHeaders }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:device_code', - device_code, client_id: config.clientId, + user_code: data.user_code, + code_verifier: codeVerifier, }), }); - if (tokenRes.ok) { - return (await tokenRes.json()) as OAuthTokens; + if (!tokenRes.ok) { + throw new CLIError( + `Device code authorization failed: HTTP ${tokenRes.status}`, + ExitCode.AUTH, + ); } - const err = (await tokenRes.json()) as { error: string }; - if (err.error === 'authorization_pending') continue; - if (err.error === 'slow_down') { - await new Promise(r => setTimeout(r, 5000)); - continue; + const tokenData = (await tokenRes.json()) as { + status: string; + access_token?: string; + refresh_token?: string; + expired_in?: number; + resource_url?: string; + }; + + if (tokenData.status === 'success' && tokenData.access_token) { + return { + access_token: tokenData.access_token, + refresh_token: tokenData.refresh_token ?? '', + expired_in: tokenData.expired_in ?? 0, + token_type: 'Bearer', + resource_url: tokenData.resource_url, + }; } + if (tokenData.status === 'pending') continue; + throw new CLIError( - `Device code authorization failed: ${err.error}`, + `Device code authorization failed: ${tokenData.status}`, ExitCode.AUTH, ); } diff --git a/src/auth/refresh.ts b/src/auth/refresh.ts index e544dea..b0eb594 100644 --- a/src/auth/refresh.ts +++ b/src/auth/refresh.ts @@ -3,31 +3,37 @@ 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 DEFAULT_TOKEN_URL = 'https://account.minimax.io/oauth2/token'; +const DEFAULT_CLIENT_ID = '659cf4c1-615c-45f6-a5f6-4bf15eb476e5'; const MAX_REFRESH_RETRIES = 2; const RETRY_DELAY_MS = 500; export async function refreshAccessToken( refreshToken: string, + tokenUrl: string = DEFAULT_TOKEN_URL, + clientId: string = DEFAULT_CLIENT_ID, ): Promise { + const lane = process.env.BEDROCK_LANE; + const extraHeaders: Record = lane ? { bedrock_lane: lane } : {}; + if (process.env.X_USER_PRE) extraHeaders['X-User-Pre'] = 'true'; + let lastErr: Error | null = null; for (let attempt = 0; attempt <= MAX_REFRESH_RETRIES; attempt++) { if (attempt > 0) { - // Exponential backoff before retry await new Promise(r => setTimeout(r, RETRY_DELAY_MS * attempt)); } let res: Response; try { - res = await fetch(TOKEN_URL, { + res = await fetch(tokenUrl, { method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, + headers: { "Content-Type": "application/x-www-form-urlencoded", ...extraHeaders }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, + client_id: clientId, }), signal: AbortSignal.timeout(10_000), }); @@ -42,11 +48,10 @@ export async function refreshAccessToken( ? "Token refresh timed out — auth server did not respond within 10 s." : `Token refresh failed: ${err instanceof Error ? err.message : String(err)}`, ); - continue; // retry + continue; } if (!res.ok) { - // 4xx errors won't recover with retry if (res.status >= 400 && res.status < 500) { throw new CLIError( "OAuth session expired and could not be refreshed.", @@ -55,14 +60,34 @@ export async function refreshAccessToken( ); } lastErr = new Error(`Token refresh failed: HTTP ${res.status}`); - continue; // retry 5xx errors + continue; + } + + const body = (await res.json()) as { + status: string; + access_token?: string; + refresh_token?: string; + expired_in?: number; + resource_url?: string; + }; + + if (body.status !== 'success' || !body.access_token) { + throw new CLIError( + 'OAuth refresh failed.', + ExitCode.AUTH, + 'Re-authenticate: mmx auth login', + ); } - const data = (await res.json()) as OAuthTokens; - return data; + return { + access_token: body.access_token, + refresh_token: body.refresh_token ?? refreshToken, + expired_in: body.expired_in ?? 0, + token_type: 'Bearer', + resource_url: body.resource_url, + }; } - // All retries exhausted throw new CLIError( `Token refresh failed after ${MAX_REFRESH_RETRIES + 1} attempts: ${lastErr?.message}`, ExitCode.AUTH, @@ -70,22 +95,26 @@ export async function refreshAccessToken( ); } -export async function ensureFreshToken(creds: CredentialFile): Promise { +export async function ensureFreshToken( + creds: CredentialFile, + tokenUrl?: string, + clientId?: string, +): Promise { const expiresAt = new Date(creds.expires_at).getTime(); - const bufferMs = 5 * 60 * 1000; // 5 minutes + const bufferMs = 5 * 60 * 1000; if (Date.now() < expiresAt - bufferMs) { return creds.access_token; } - // Token expired or about to expire — refresh - const tokens = await refreshAccessToken(creds.refresh_token); + const tokens = await refreshAccessToken(creds.refresh_token, tokenUrl, clientId); const updated: 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", + expires_at: new Date(tokens.expired_in).toISOString(), // expired_in is Unix timestamp (ms) + token_type: 'Bearer', + resource_url: tokens.resource_url ?? creds.resource_url, account: creds.account, }; diff --git a/src/auth/resolver.ts b/src/auth/resolver.ts index eb9a11a..d28cef8 100644 --- a/src/auth/resolver.ts +++ b/src/auth/resolver.ts @@ -14,7 +14,7 @@ export async function resolveCredential(config: Config): Promise { +export async function ensureAuth(config: Config): Promise { if (config.apiKey || config.fileApiKey) return; + // Check existing OAuth credentials + const existingOAuth = await loadCredentials(); + if (existingOAuth) return; + const envKey = process.env.MINIMAX_API_KEY; let key: string | undefined; @@ -26,11 +33,50 @@ export async function ensureApiKey(config: Config): Promise { if (!key) { if (!isInteractive({ nonInteractive: config.nonInteractive })) { throw new CLIError( - 'No API key found.', + 'No credentials found.', ExitCode.AUTH, - 'Set env var: export MINIMAX_API_KEY=sk-xxxxx\nPass directly: --api-key sk-xxxxx', + 'Log in: mmx auth login\nPass directly: --api-key sk-xxxxx', ); } + + const { select } = await import('@clack/prompts'); + const method = await select({ + message: 'How would you like to authenticate?', + options: [ + { value: 'oauth', label: 'Log in with MiniMax account (OAuth)' }, + { value: 'api-key', label: 'Enter API key manually' }, + ], + }); + + if (typeof method === 'symbol') { + // User pressed Ctrl+C + throw new CLIError('Authentication cancelled.', ExitCode.AUTH); + } + + if (method === 'oauth') { + const oauthConfig: OAuthConfig = { + clientId: '659cf4c1-615c-45f6-a5f6-4bf15eb476e5', + clientName: 'MiniMax CLI', + authorizationUrl: `${config.platformHost}/oauth-authorize`, + tokenUrl: `${config.oauthApiHost}/oauth2/token`, + deviceCodeUrl: `${config.oauthApiHost}/oauth2/device/code`, + scopes: ['openid', 'profile', 'coding_plan'], + callbackPort: 18991, + }; + const tokens = await startDeviceCodeFlow(oauthConfig); + const creds: CredentialFile = { + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + expires_at: new Date(tokens.expired_in).toISOString(), + token_type: 'Bearer', + resource_url: tokens.resource_url, + }; + await saveCredentials(creds); + process.stderr.write('Logged in successfully.\n'); + return; + } + + // api-key method const input = await promptText({ message: 'Enter your MiniMax API key:' }); if (!input) throw new CLIError('API key is required.', ExitCode.AUTH); key = input; diff --git a/src/auth/types.ts b/src/auth/types.ts index 0f39fbe..4b87289 100644 --- a/src/auth/types.ts +++ b/src/auth/types.ts @@ -3,8 +3,9 @@ export type AuthMethod = 'api-key' | 'oauth'; export interface OAuthTokens { access_token: string; refresh_token: string; - expires_in: number; + expired_in: number; // milliseconds token_type: 'Bearer'; + resource_url?: string; } export interface CredentialFile { @@ -12,6 +13,7 @@ export interface CredentialFile { refresh_token: string; expires_at: string; // ISO 8601 token_type: 'Bearer'; + resource_url?: string; account?: string; } diff --git a/src/client/endpoints.ts b/src/client/endpoints.ts index b8337a0..95f164c 100644 --- a/src/client/endpoints.ts +++ b/src/client/endpoints.ts @@ -1,5 +1,6 @@ export function chatEndpoint(baseUrl: string): string { - return `${baseUrl}/anthropic/v1/messages`; + const base = baseUrl.replace(/\/anthropic\/?$/, ''); + return `${base}/anthropic/v1/messages`; } export function speechEndpoint(baseUrl: string): string { diff --git a/src/client/http.ts b/src/client/http.ts index c77ba79..eb2f2a2 100644 --- a/src/client/http.ts +++ b/src/client/http.ts @@ -26,6 +26,14 @@ export async function request(config: Config, opts: RequestOpts): Promise', description: 'Auth method: oauth (default), api-key' }, { flag: '--api-key ', description: 'API key to store' }, - { flag: '--no-browser', description: 'Use device-code flow instead of browser' }, ], examples: [ 'mmx auth login', - 'mmx auth login --no-browser', 'mmx auth login --api-key sk-xxxxx', 'mmx auth login --method api-key --api-key sk-xxxxx', ], @@ -112,18 +111,48 @@ export default defineCommand({ return; } - let tokens; - if (flags.noBrowser) { - tokens = await startDeviceCodeFlow(); - } else { - tokens = await startBrowserFlow(); + // If no region was explicitly specified via flag or env, let the user choose interactively + let region = config.region; + if (!flags.region && !process.env.MINIMAX_REGION) { + if (isInteractive({ nonInteractive: config.nonInteractive })) { + const { select } = await import('@clack/prompts'); + const chosen = await select({ + message: 'Select your region', + options: [ + { value: 'cn', label: 'minimax.com (China)' }, + { value: 'global', label: 'minimax.io (Global)' }, + ], + }); + if (typeof chosen === 'string') { + region = chosen as Region; + const existing = readConfigFile() as Record; + existing.region = region; + await writeConfigFile(existing); + } + } } + const platformHost = process.env.MINIMAX_PLATFORM_URL || PLATFORM_HOSTS[region]; + const oauthApiHost = process.env.MINIMAX_AUTH_URL || OAUTH_API_HOSTS[region]; + + const oauthConfig: OAuthConfig = { + clientId: '659cf4c1-615c-45f6-a5f6-4bf15eb476e5', + clientName: 'MiniMax CLI', + authorizationUrl: `${platformHost}/oauth-authorize`, + tokenUrl: `${oauthApiHost}/oauth2/token`, + deviceCodeUrl: `${oauthApiHost}/oauth2/device/code`, + scopes: ['openid', 'profile', 'coding_plan'], + callbackPort: 18991, + }; + + const tokens = await startDeviceCodeFlow(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(), + expires_at: new Date(tokens.expired_in).toISOString(), // expired_in is Unix timestamp (ms) token_type: 'Bearer', + resource_url: tokens.resource_url, }; await saveCredentials(creds); diff --git a/src/commands/auth/refresh.ts b/src/commands/auth/refresh.ts index 2328062..db8dd18 100644 --- a/src/commands/auth/refresh.ts +++ b/src/commands/auth/refresh.ts @@ -31,13 +31,17 @@ export default defineCommand({ return; } - const tokens = await refreshAccessToken(creds.refresh_token); + const tokens = await refreshAccessToken( + creds.refresh_token, + `${config.oauthApiHost}/oauth2/token`, + ); const updated: CredentialFile = { access_token: tokens.access_token, refresh_token: tokens.refresh_token, - expires_at: new Date(Date.now() + tokens.expires_in * 1000).toISOString(), + expires_at: new Date(tokens.expired_in).toISOString(), // expired_in is Unix timestamp (ms) token_type: 'Bearer', + resource_url: tokens.resource_url ?? creds.resource_url, account: creds.account, }; diff --git a/src/config/loader.ts b/src/config/loader.ts index 5d40ec9..af8e7ec 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -1,6 +1,6 @@ import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs'; -import { parseConfigFile, REGIONS, type Config, type ConfigFile, type Region } from './schema'; -import { ensureConfigDir, getConfigPath } from './paths'; +import { parseConfigFile, REGIONS, PLATFORM_HOSTS, OAUTH_API_HOSTS, type Config, type ConfigFile, type Region } from './schema'; +import { ensureConfigDir, getConfigPath, getCredentialsPath } from './paths'; import { detectOutputFormat, type OutputFormat } from '../output/formatter'; import { CLIError } from '../errors/base'; import { ExitCode } from '../errors/codes'; @@ -28,6 +28,17 @@ export async function writeConfigFile(data: Record): Promise = { + cn: 'https://platform.minimaxi.com', + global: 'https://platform.minimax.io', +}; +export const OAUTH_API_HOSTS: Record = { + cn: 'https://account.minimaxi.com', + global: 'https://account.minimax.io', +}; export interface ConfigFile { api_key?: string; region?: Region; @@ -52,6 +60,8 @@ export interface Config { configPath?: string; region: Region; baseUrl: string; + platformHost: string; + oauthApiHost: string; output: 'text' | 'json'; timeout: number; defaultTextModel?: string; diff --git a/src/main.ts b/src/main.ts index 0ad712f..b1dff49 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,7 +7,7 @@ import { detectRegion, saveDetectedRegion } from './config/detect-region'; import { REGIONS, type Region } from './config/schema'; import { checkForUpdate, getPendingUpdateNotification } from './update/checker'; import { loadCredentials } from './auth/credentials'; -import { ensureApiKey } from './auth/setup'; +import { ensureAuth } from './auth/setup'; import { CLI_VERSION } from './version'; import { ProxyAgent, setGlobalDispatcher } from 'undici'; @@ -75,7 +75,8 @@ async function main() { await quotaCmd.execute(config, flags); } else { process.stderr.write(' Not logged in.\n'); - process.stderr.write(' mmx auth login --api-key sk-xxxxx\n\n'); + process.stderr.write(' mmx auth login Log in with MiniMax account by OAuth\n'); + process.stderr.write(' mmx auth login --api-key Log in with API key\n\n'); } process.exit(0); } @@ -91,7 +92,7 @@ async function main() { (cmd) => cmd.every((c, i) => commandPath[i] === c), ); if (needsAuthSetup) { - await ensureApiKey(config); + await ensureAuth(config); } if (config.needsRegionDetection) { diff --git a/test/auth/resolver.test.ts b/test/auth/resolver.test.ts index f967803..aec310b 100644 --- a/test/auth/resolver.test.ts +++ b/test/auth/resolver.test.ts @@ -5,6 +5,8 @@ import type { Config } from '../../src/config/schema'; function makeConfig(overrides: Partial = {}): Config { return { region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text', timeout: 300, diff --git a/test/client/http.test.ts b/test/client/http.test.ts index 07c0aeb..a293652 100644 --- a/test/client/http.test.ts +++ b/test/client/http.test.ts @@ -8,6 +8,8 @@ function makeConfig(baseUrl: string): Config { return { apiKey: 'test-api-key', region: 'global', + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl, output: 'text', timeout: 10, diff --git a/test/commands/auth/login.test.ts b/test/commands/auth/login.test.ts index 8095acc..df3c81a 100644 --- a/test/commands/auth/login.test.ts +++ b/test/commands/auth/login.test.ts @@ -10,6 +10,8 @@ describe('auth login command', () => { it('requires api key when method is api-key', async () => { const config = { region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/auth/logout.test.ts b/test/commands/auth/logout.test.ts index 32bfdbf..cf95db9 100644 --- a/test/commands/auth/logout.test.ts +++ b/test/commands/auth/logout.test.ts @@ -9,6 +9,8 @@ describe('auth logout command', () => { it('handles dry run', async () => { const config = { region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/auth/refresh.test.ts b/test/commands/auth/refresh.test.ts index 56d8fa6..ee849d5 100644 --- a/test/commands/auth/refresh.test.ts +++ b/test/commands/auth/refresh.test.ts @@ -9,6 +9,8 @@ describe('auth refresh command', () => { it('errors when not using OAuth', async () => { const config = { region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/auth/status.test.ts b/test/commands/auth/status.test.ts index ae93f2f..724af22 100644 --- a/test/commands/auth/status.test.ts +++ b/test/commands/auth/status.test.ts @@ -9,6 +9,8 @@ describe('auth status command', () => { it('shows not authenticated when no credentials', async () => { const config = { region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'json' as const, timeout: 10, diff --git a/test/commands/config/set.test.ts b/test/commands/config/set.test.ts index 53f085e..f6cb294 100644 --- a/test/commands/config/set.test.ts +++ b/test/commands/config/set.test.ts @@ -15,6 +15,8 @@ describe('config set command', () => { it('requires key and value', async () => { const config = { region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, @@ -44,6 +46,8 @@ describe('config set command', () => { it('validates config key', async () => { const config = { region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/config/show.test.ts b/test/commands/config/show.test.ts index e063b7c..46ce90c 100644 --- a/test/commands/config/show.test.ts +++ b/test/commands/config/show.test.ts @@ -21,6 +21,8 @@ describe('config show command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'json' as const, timeout: 300, diff --git a/test/commands/file/upload.test.ts b/test/commands/file/upload.test.ts index c48994d..daa562c 100644 --- a/test/commands/file/upload.test.ts +++ b/test/commands/file/upload.test.ts @@ -10,6 +10,8 @@ describe('file upload command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/image/generate.test.ts b/test/commands/image/generate.test.ts index fb29b6a..a07ffd5 100644 --- a/test/commands/image/generate.test.ts +++ b/test/commands/image/generate.test.ts @@ -10,6 +10,8 @@ describe('image generate command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/music/generate.test.ts b/test/commands/music/generate.test.ts index e5d0b16..8af773f 100644 --- a/test/commands/music/generate.test.ts +++ b/test/commands/music/generate.test.ts @@ -4,6 +4,8 @@ import { default as generateCommand } from '../../../src/commands/music/generate const baseConfig = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/quota/show.test.ts b/test/commands/quota/show.test.ts index ebb88fd..a836d73 100644 --- a/test/commands/quota/show.test.ts +++ b/test/commands/quota/show.test.ts @@ -10,6 +10,8 @@ describe('quota show command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/search/query.test.ts b/test/commands/search/query.test.ts index 6784be1..94c477e 100644 --- a/test/commands/search/query.test.ts +++ b/test/commands/search/query.test.ts @@ -10,6 +10,8 @@ describe('search query command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/speech/synthesize.test.ts b/test/commands/speech/synthesize.test.ts index 8a49063..c1818be 100644 --- a/test/commands/speech/synthesize.test.ts +++ b/test/commands/speech/synthesize.test.ts @@ -10,6 +10,8 @@ describe('speech synthesize command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, @@ -40,6 +42,8 @@ describe('speech synthesize command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'json' as const, timeout: 10, diff --git a/test/commands/text/chat.test.ts b/test/commands/text/chat.test.ts index 37ad015..6e6d795 100644 --- a/test/commands/text/chat.test.ts +++ b/test/commands/text/chat.test.ts @@ -23,6 +23,8 @@ describe('text chat command', () => { const config: Config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: server.url, output: 'json', timeout: 10, @@ -66,6 +68,8 @@ describe('text chat command', () => { const config: Config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'json', timeout: 10, diff --git a/test/commands/video/download.test.ts b/test/commands/video/download.test.ts index dc8b14e..04ee162 100644 --- a/test/commands/video/download.test.ts +++ b/test/commands/video/download.test.ts @@ -10,6 +10,8 @@ describe('video download command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, @@ -40,6 +42,8 @@ describe('video download command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/video/generate.test.ts b/test/commands/video/generate.test.ts index 9845a09..c654a35 100644 --- a/test/commands/video/generate.test.ts +++ b/test/commands/video/generate.test.ts @@ -10,6 +10,8 @@ describe('video generate command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, diff --git a/test/commands/video/task-get.test.ts b/test/commands/video/task-get.test.ts index 29046d8..246dcad 100644 --- a/test/commands/video/task-get.test.ts +++ b/test/commands/video/task-get.test.ts @@ -18,6 +18,8 @@ describe('video task get command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, @@ -54,6 +56,8 @@ describe('video task get command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: server.url, output: 'json' as const, timeout: 10, diff --git a/test/commands/vision/describe.test.ts b/test/commands/vision/describe.test.ts index 77db40f..c7db879 100644 --- a/test/commands/vision/describe.test.ts +++ b/test/commands/vision/describe.test.ts @@ -10,6 +10,8 @@ describe('vision describe command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10, @@ -40,6 +42,8 @@ describe('vision describe command', () => { const config = { apiKey: 'test-key', region: 'global' as const, + platformHost: 'https://platform.minimax.io', + oauthApiHost: 'https://account.minimax.io', baseUrl: 'https://api.mmx.io', output: 'text' as const, timeout: 10,