fix(feishu): bound streaming token expiry

This commit is contained in:
Peter Steinberger
2026-05-29 13:28:34 -04:00
parent 6811cee756
commit 04de01f8cf
2 changed files with 72 additions and 1 deletions

View File

@@ -342,6 +342,65 @@ describe("FeishuStreamingSession", () => {
"Final replace failed: Error: Replace card content failed with HTTP 500",
);
});
it("bounds streaming token cache lifetime when token expiry overflows", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-29T12:00:00.000Z"));
const release = vi.fn(async () => {});
const authTokens: string[] = [];
fetchWithSsrFGuardMock.mockImplementation(
async ({ url }: { url: string; init?: { body?: string } }) => {
if (url.includes("/auth/")) {
const token = `token-${authTokens.length + 1}`;
authTokens.push(token);
return {
response: {
ok: true,
json: async () => ({
code: 0,
msg: "ok",
tenant_access_token: token,
expire: Number.MAX_SAFE_INTEGER,
}),
},
release,
};
}
return {
response: {
ok: true,
json: async () => ({
code: 0,
msg: "ok",
data: { card_id: `card-${authTokens.length}` },
}),
},
release,
};
},
);
const client = {
im: {
message: {
create: vi.fn(async () => ({ code: 0, msg: "ok", data: { message_id: "om_1" } })),
},
},
} as never;
await new FeishuStreamingSession(client, {
appId: "app_unsafe_token_expiry",
appSecret: "secret",
}).start("chat_id", "open_id");
expect(authTokens).toEqual(["token-1"]);
vi.setSystemTime(Date.now() + 7200 * 1000 - 60_000 + 1);
await new FeishuStreamingSession(client, {
appId: "app_unsafe_token_expiry",
appSecret: "secret",
}).start("chat_id", "open_id");
expect(authTokens).toEqual(["token-1", "token-2"]);
});
});
describe("mergeStreamingText", () => {

View File

@@ -3,6 +3,7 @@
*/
import type { Client } from "@larksuiteoapi/node-sdk";
import { resolveExpiresAtMsFromDurationSeconds } from "openclaw/plugin-sdk/number-runtime";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { getFeishuUserAgent } from "./client.js";
import { resolveFeishuCardTemplate, type CardHeaderConfig } from "./send.js";
@@ -42,10 +43,21 @@ type StreamingStartOptions = {
const STREAMING_UPDATE_THROTTLE_MS = 160;
const STREAMING_SIGNIFICANT_DELTA_CHARS = 18;
const FEISHU_STREAMING_TOKEN_DEFAULT_LIFETIME_SECONDS = 7200;
// Token cache (keyed by domain + appId)
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
function resolveStreamingTokenExpiresAt(value: unknown): number {
if (typeof value === "number" && Number.isFinite(value) && value <= 0) {
return Date.now();
}
return (
resolveExpiresAtMsFromDurationSeconds(value) ??
Date.now() + FEISHU_STREAMING_TOKEN_DEFAULT_LIFETIME_SECONDS * 1000
);
}
function resolveApiBase(domain?: FeishuDomain): string {
if (domain === "lark") {
return "https://open.larksuite.com/open-apis";
@@ -103,7 +115,7 @@ async function getToken(creds: Credentials): Promise<string> {
}
tokenCache.set(key, {
token: data.tenant_access_token,
expiresAt: Date.now() + (data.expire ?? 7200) * 1000,
expiresAt: resolveStreamingTokenExpiresAt(data.expire),
});
return data.tenant_access_token;
}