Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
305
extensions/minimax/oauth.ts
Normal file
305
extensions/minimax/oauth.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
// Minimax plugin module implements oauth behavior.
|
||||
import { randomBytes, randomUUID } from "node:crypto";
|
||||
import {
|
||||
MAX_DATE_TIMESTAMP_MS,
|
||||
asSafeIntegerInRange,
|
||||
resolveExpiresAtMsFromDurationOrEpoch,
|
||||
resolvePositiveTimerTimeoutMs,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { generatePkceVerifierChallenge, toFormUrlEncoded } from "openclaw/plugin-sdk/provider-auth";
|
||||
import {
|
||||
readProviderJsonResponse,
|
||||
readResponseTextLimited,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
|
||||
export type MiniMaxRegion = "cn" | "global";
|
||||
|
||||
const MINIMAX_OAUTH_CONFIG = {
|
||||
cn: {
|
||||
baseUrl: "https://api.minimaxi.com",
|
||||
oauthBaseUrl: "https://account.minimaxi.com",
|
||||
clientId: "78257093-7e40-4613-99e0-527b14b39113",
|
||||
},
|
||||
global: {
|
||||
baseUrl: "https://api.minimax.io",
|
||||
oauthBaseUrl: "https://account.minimax.io",
|
||||
clientId: "78257093-7e40-4613-99e0-527b14b39113",
|
||||
},
|
||||
} as const;
|
||||
|
||||
const MINIMAX_OAUTH_SCOPE = "group_id profile model.completion";
|
||||
const MINIMAX_OAUTH_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:user_code";
|
||||
const MINIMAX_RELATIVE_EXPIRY_SECONDS_THRESHOLD = 1_000_000_000;
|
||||
const MINIMAX_ABSOLUTE_EXPIRY_MS_THRESHOLD = 1_000_000_000_000;
|
||||
const MINIMAX_OAUTH_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
||||
|
||||
function getOAuthEndpoints(region: MiniMaxRegion) {
|
||||
const config = MINIMAX_OAUTH_CONFIG[region];
|
||||
return {
|
||||
codeEndpoint: `${config.oauthBaseUrl}/oauth2/device/code`,
|
||||
tokenEndpoint: `${config.oauthBaseUrl}/oauth2/token`,
|
||||
clientId: config.clientId,
|
||||
baseUrl: config.baseUrl,
|
||||
hostname: new URL(config.oauthBaseUrl).hostname,
|
||||
};
|
||||
}
|
||||
|
||||
type MiniMaxOAuthAuthorization = {
|
||||
user_code: string;
|
||||
verification_uri: string;
|
||||
expired_in: number;
|
||||
interval?: number;
|
||||
state: string;
|
||||
};
|
||||
|
||||
type MiniMaxOAuthToken = {
|
||||
access: string;
|
||||
refresh: string;
|
||||
expires: number;
|
||||
resourceUrl?: string;
|
||||
notification_message?: string;
|
||||
};
|
||||
|
||||
type TokenPending = { status: "pending"; message?: string };
|
||||
|
||||
type TokenResult =
|
||||
| { status: "success"; token: MiniMaxOAuthToken }
|
||||
| TokenPending
|
||||
| { status: "error"; message: string };
|
||||
|
||||
/**
|
||||
* Normalize MiniMax token endpoint `expired_in` values to the auth-profile
|
||||
* contract: absolute Unix milliseconds.
|
||||
*/
|
||||
export function normalizeOAuthExpires(expiredIn: unknown, now = Date.now()): number | undefined {
|
||||
return resolveExpiresAtMsFromDurationOrEpoch(expiredIn, {
|
||||
nowMs: now,
|
||||
relativeSecondsThreshold: MINIMAX_RELATIVE_EXPIRY_SECONDS_THRESHOLD,
|
||||
absoluteMillisecondsThreshold: MINIMAX_ABSOLUTE_EXPIRY_MS_THRESHOLD,
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeOAuthAuthorizationExpires(expiredIn: unknown): number | undefined {
|
||||
return asSafeIntegerInRange(expiredIn, { min: 1, max: MAX_DATE_TIMESTAMP_MS });
|
||||
}
|
||||
|
||||
function generatePkce(): { verifier: string; challenge: string; state: string } {
|
||||
const { verifier, challenge } = generatePkceVerifierChallenge();
|
||||
const state = randomBytes(16).toString("base64url");
|
||||
return { verifier, challenge, state };
|
||||
}
|
||||
|
||||
async function requestOAuthCode(params: {
|
||||
challenge: string;
|
||||
state: string;
|
||||
region: MiniMaxRegion;
|
||||
}): Promise<MiniMaxOAuthAuthorization> {
|
||||
const endpoints = getOAuthEndpoints(params.region);
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: endpoints.codeEndpoint,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
"x-request-id": randomUUID(),
|
||||
},
|
||||
body: toFormUrlEncoded({
|
||||
response_type: "code",
|
||||
client_id: endpoints.clientId,
|
||||
scope: MINIMAX_OAUTH_SCOPE,
|
||||
code_challenge: params.challenge,
|
||||
code_challenge_method: "S256",
|
||||
state: params.state,
|
||||
}),
|
||||
},
|
||||
policy: { allowedHostnames: [endpoints.hostname] },
|
||||
auditContext: "minimax.oauth.code",
|
||||
});
|
||||
try {
|
||||
if (!response.ok) {
|
||||
const text = await readResponseTextLimited(response, MINIMAX_OAUTH_ERROR_BODY_LIMIT_BYTES);
|
||||
throw new Error(`MiniMax OAuth authorization failed: ${text || response.statusText}`);
|
||||
}
|
||||
|
||||
const payload = (await readProviderJsonResponse(
|
||||
response,
|
||||
"minimax.oauth-code",
|
||||
)) as MiniMaxOAuthAuthorization & { error?: string };
|
||||
if (!payload.user_code || !payload.verification_uri) {
|
||||
throw new Error(
|
||||
payload.error ??
|
||||
"MiniMax OAuth authorization returned an incomplete payload (missing user_code or verification_uri).",
|
||||
);
|
||||
}
|
||||
if (payload.state !== params.state) {
|
||||
throw new Error("MiniMax OAuth state mismatch: possible CSRF attack or session corruption.");
|
||||
}
|
||||
const expiredIn = normalizeOAuthAuthorizationExpires(payload.expired_in);
|
||||
if (expiredIn === undefined) {
|
||||
throw new Error("MiniMax OAuth authorization returned invalid expired_in.");
|
||||
}
|
||||
return { ...payload, expired_in: expiredIn };
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
async function pollOAuthToken(params: {
|
||||
userCode: string;
|
||||
verifier: string;
|
||||
region: MiniMaxRegion;
|
||||
}): Promise<TokenResult> {
|
||||
const endpoints = getOAuthEndpoints(params.region);
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: endpoints.tokenEndpoint,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: toFormUrlEncoded({
|
||||
grant_type: MINIMAX_OAUTH_GRANT_TYPE,
|
||||
client_id: endpoints.clientId,
|
||||
user_code: params.userCode,
|
||||
code_verifier: params.verifier,
|
||||
}),
|
||||
},
|
||||
policy: { allowedHostnames: [endpoints.hostname] },
|
||||
auditContext: "minimax.oauth.token",
|
||||
});
|
||||
try {
|
||||
return await parseMiniMaxOAuthTokenResponse(response);
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
async function parseMiniMaxOAuthTokenResponse(response: Response): Promise<TokenResult> {
|
||||
const text = await readResponseTextLimited(response, MINIMAX_OAUTH_ERROR_BODY_LIMIT_BYTES);
|
||||
let payload:
|
||||
| {
|
||||
status?: string;
|
||||
base_resp?: { status_code?: number; status_msg?: string };
|
||||
}
|
||||
| undefined;
|
||||
if (text) {
|
||||
try {
|
||||
payload = JSON.parse(text) as typeof payload;
|
||||
} catch {
|
||||
payload = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
status: "error",
|
||||
message:
|
||||
(payload?.base_resp?.status_msg ?? text) || "MiniMax OAuth failed to parse response.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
return { status: "error", message: "MiniMax OAuth failed to parse response." };
|
||||
}
|
||||
|
||||
const tokenPayload = payload as {
|
||||
status: string;
|
||||
access_token?: string | null;
|
||||
refresh_token?: string | null;
|
||||
expired_in?: unknown;
|
||||
token_type?: string;
|
||||
resource_url?: string;
|
||||
notification_message?: string;
|
||||
};
|
||||
|
||||
if (tokenPayload.status === "error") {
|
||||
return { status: "error", message: "An error occurred. Please try again later" };
|
||||
}
|
||||
|
||||
if (tokenPayload.status !== "success") {
|
||||
return { status: "pending", message: "current user code is not authorized" };
|
||||
}
|
||||
|
||||
if (!tokenPayload.access_token || !tokenPayload.refresh_token || !tokenPayload.expired_in) {
|
||||
return { status: "error", message: "MiniMax OAuth returned incomplete token payload." };
|
||||
}
|
||||
const expires = normalizeOAuthExpires(tokenPayload.expired_in);
|
||||
if (expires === undefined) {
|
||||
return { status: "error", message: "MiniMax OAuth returned invalid token expiry." };
|
||||
}
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
token: {
|
||||
access: tokenPayload.access_token,
|
||||
refresh: tokenPayload.refresh_token,
|
||||
expires,
|
||||
resourceUrl: tokenPayload.resource_url,
|
||||
notification_message: tokenPayload.notification_message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function loginMiniMaxPortalOAuth(params: {
|
||||
openUrl: (url: string) => Promise<void>;
|
||||
note: (message: string, title?: string) => Promise<void>;
|
||||
progress: { update: (message: string) => void; stop: (message?: string) => void };
|
||||
region?: MiniMaxRegion;
|
||||
}): Promise<MiniMaxOAuthToken> {
|
||||
// Ensure env-based proxy dispatcher is active before any outbound fetch calls.
|
||||
// Without this, HTTP_PROXY/HTTPS_PROXY env vars are silently ignored (#51619).
|
||||
ensureGlobalUndiciEnvProxyDispatcher();
|
||||
const region = params.region ?? "global";
|
||||
const { verifier, challenge, state } = generatePkce();
|
||||
const oauth = await requestOAuthCode({ challenge, state, region });
|
||||
const verificationUrl = oauth.verification_uri;
|
||||
|
||||
const noteLines = [
|
||||
`Open ${verificationUrl} to approve access.`,
|
||||
`If prompted, enter the code ${oauth.user_code}.`,
|
||||
`Interval: ${oauth.interval ?? "default (2000ms)"}, Expires at: ${new Date(oauth.expired_in).toISOString()}`,
|
||||
];
|
||||
await params.note(noteLines.join("\n"), "MiniMax OAuth");
|
||||
|
||||
try {
|
||||
await params.openUrl(verificationUrl);
|
||||
} catch {
|
||||
// Fall back to manual copy/paste if browser open fails.
|
||||
}
|
||||
|
||||
let pollIntervalMs = resolvePositiveTimerTimeoutMs(oauth.interval, 2000);
|
||||
// The authorization endpoint returns an absolute millisecond deadline.
|
||||
const expireTimeMs = oauth.expired_in;
|
||||
|
||||
while (Date.now() < expireTimeMs) {
|
||||
params.progress.update("Waiting for MiniMax OAuth approval…");
|
||||
const result = await pollOAuthToken({
|
||||
userCode: oauth.user_code,
|
||||
verifier,
|
||||
region,
|
||||
});
|
||||
|
||||
if (result.status === "success") {
|
||||
return result.token;
|
||||
}
|
||||
|
||||
if (result.status === "error") {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
|
||||
const remainingMs = Math.max(0, expireTimeMs - Date.now());
|
||||
if (remainingMs <= 0) {
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, Math.min(pollIntervalMs, remainingMs));
|
||||
});
|
||||
pollIntervalMs = Math.max(pollIntervalMs, 2000);
|
||||
}
|
||||
|
||||
throw new Error("MiniMax OAuth timed out before authorization completed.");
|
||||
}
|
||||
Reference in New Issue
Block a user