mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(minimax): normalize OAuth token expiry to absolute millisecond timestamp (#83480)
* fix(minimax): normalize OAuth token expiry to absolute millisecond timestamp MiniMax returns expired_in from the token endpoint as a relative duration in seconds (standard OAuth expires_in semantics), but the auth profile store's hasUsableOAuthCredential() expects an absolute millisecond timestamp. Without conversion the token appears perpetually expired, triggering a slow OAuth refresh network call to api.minimaxi.com on every request — the root cause of the 30-50s auth-stage delay. Fixes #83449. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(minimax): cover oauth expiry normalization * fix: polish minimax oauth expiry normalization (#83480) (thanks @NianJiuZst) * fix: update minimax raw fetch allowlist (#83480) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- MiniMax: store OAuth token expiry as an absolute millisecond timestamp so OAuth profiles no longer appear expired on every request. (#83480) Thanks @NianJiuZst.
|
||||
- Gateway/restart: honor the configured restart drain budget for embedded runs and avoid spending the deferral timeout twice after forced restart timeouts. (#85708) Thanks @Kaspre.
|
||||
- Gateway/boot: run `BOOT.md` startup checks in an isolated boot session so gateway restarts do not overwrite the agent's main session mapping. (#85479)
|
||||
- Meeting Notes: include a speaker-labeled transcript section in generated summaries so Discord group voice captures show who said each captured utterance.
|
||||
|
||||
16
extensions/minimax/oauth.test.ts
Normal file
16
extensions/minimax/oauth.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeOAuthExpires } from "./oauth.js";
|
||||
|
||||
describe("normalizeOAuthExpires", () => {
|
||||
it("converts relative expiry seconds into an absolute millisecond timestamp", () => {
|
||||
expect(normalizeOAuthExpires(86_400, 1_700_000_000_000)).toBe(1_700_086_400_000);
|
||||
});
|
||||
|
||||
it("converts Unix second timestamps into milliseconds", () => {
|
||||
expect(normalizeOAuthExpires(1_700_000_000)).toBe(1_700_000_000_000);
|
||||
});
|
||||
|
||||
it("preserves absolute millisecond timestamps", () => {
|
||||
expect(normalizeOAuthExpires(1_700_000_000_000)).toBe(1_700_000_000_000);
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,8 @@ const MINIMAX_OAUTH_CONFIG = {
|
||||
|
||||
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;
|
||||
|
||||
function getOAuthEndpoints(region: MiniMaxRegion) {
|
||||
const config = MINIMAX_OAUTH_CONFIG[region];
|
||||
@@ -51,6 +53,20 @@ type TokenResult =
|
||||
| TokenPending
|
||||
| { status: "error"; message: string };
|
||||
|
||||
/**
|
||||
* Normalize MiniMax token endpoint `expired_in` values to the auth-profile
|
||||
* contract: absolute Unix milliseconds.
|
||||
*/
|
||||
export function normalizeOAuthExpires(expiredIn: number, now = Date.now()): number {
|
||||
if (expiredIn < MINIMAX_RELATIVE_EXPIRY_SECONDS_THRESHOLD) {
|
||||
return now + expiredIn * 1000;
|
||||
}
|
||||
if (expiredIn < MINIMAX_ABSOLUTE_EXPIRY_MS_THRESHOLD) {
|
||||
return expiredIn * 1000;
|
||||
}
|
||||
return expiredIn;
|
||||
}
|
||||
|
||||
function generatePkce(): { verifier: string; challenge: string; state: string } {
|
||||
const { verifier, challenge } = generatePkceVerifierChallenge();
|
||||
const state = randomBytes(16).toString("base64url");
|
||||
@@ -172,7 +188,7 @@ async function pollOAuthToken(params: {
|
||||
token: {
|
||||
access: tokenPayload.access_token,
|
||||
refresh: tokenPayload.refresh_token,
|
||||
expires: tokenPayload.expired_in,
|
||||
expires: normalizeOAuthExpires(tokenPayload.expired_in),
|
||||
resourceUrl: tokenPayload.resource_url,
|
||||
notification_message: tokenPayload.notification_message,
|
||||
},
|
||||
@@ -196,7 +212,7 @@ export async function loginMiniMaxPortalOAuth(params: {
|
||||
const noteLines = [
|
||||
`Open ${verificationUrl} to approve access.`,
|
||||
`If prompted, enter the code ${oauth.user_code}.`,
|
||||
`Interval: ${oauth.interval ?? "default (2000ms)"}, Expires at: ${oauth.expired_in} unix timestamp`,
|
||||
`Interval: ${oauth.interval ?? "default (2000ms)"}, Expires at: ${new Date(oauth.expired_in).toISOString()}`,
|
||||
];
|
||||
await params.note(noteLines.join("\n"), "MiniMax OAuth");
|
||||
|
||||
@@ -207,6 +223,7 @@ export async function loginMiniMaxPortalOAuth(params: {
|
||||
}
|
||||
|
||||
let pollIntervalMs = oauth.interval ? oauth.interval : 2000;
|
||||
// The authorization endpoint returns an absolute millisecond deadline.
|
||||
const expireTimeMs = oauth.expired_in;
|
||||
|
||||
while (Date.now() < expireTimeMs) {
|
||||
|
||||
@@ -33,8 +33,8 @@ const allowedRawFetchCallsites = new Set([
|
||||
bundledPluginCallsite("matrix", "src/matrix/sdk/transport.ts", 112),
|
||||
bundledPluginCallsite("microsoft-foundry", "onboard.ts", 479),
|
||||
bundledPluginCallsite("microsoft", "speech-provider.ts", 140),
|
||||
bundledPluginCallsite("minimax", "oauth.ts", 66),
|
||||
bundledPluginCallsite("minimax", "oauth.ts", 107),
|
||||
bundledPluginCallsite("minimax", "oauth.ts", 82),
|
||||
bundledPluginCallsite("minimax", "oauth.ts", 123),
|
||||
bundledPluginCallsite("minimax", "tts.ts", 52),
|
||||
bundledPluginCallsite("msteams", "src/graph.ts", 47),
|
||||
bundledPluginCallsite("msteams", "src/sdk.ts", 400),
|
||||
|
||||
Reference in New Issue
Block a user