mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-04 02:04:05 +00:00
fix(gateway): bound talk handoff expiry
This commit is contained in:
@@ -107,6 +107,45 @@ describe("talk handoff store", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("expires handoffs immediately when the creation clock is invalid", () => {
|
||||
clearTalkHandoffsForTest();
|
||||
const dateNow = vi.spyOn(Date, "now").mockReturnValue(Number.NaN);
|
||||
try {
|
||||
const handoff = createTalkHandoff({
|
||||
sessionKey: "session:main",
|
||||
ttlMs: 5000,
|
||||
});
|
||||
|
||||
expect(handoff.createdAt).toBe(0);
|
||||
expect(handoff.expiresAt).toBe(0);
|
||||
expect(joinTalkHandoff(handoff.id, handoff.token)).toEqual({
|
||||
ok: false,
|
||||
reason: "expired",
|
||||
});
|
||||
} finally {
|
||||
dateNow.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("expires handoffs immediately when expiry would exceed Date bounds", () => {
|
||||
clearTalkHandoffsForTest();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(8_640_000_000_000_000));
|
||||
|
||||
const handoff = createTalkHandoff({
|
||||
sessionKey: "session:main",
|
||||
ttlMs: 5000,
|
||||
});
|
||||
|
||||
expect(handoff.expiresAt).toBe(0);
|
||||
expect(joinTalkHandoff(handoff.id, handoff.token)).toEqual({
|
||||
ok: false,
|
||||
reason: "expired",
|
||||
});
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("joins and revokes handoffs with only the bearer token", () => {
|
||||
clearTalkHandoffsForTest();
|
||||
const handoff = createTalkHandoff({ sessionKey: "session:main" });
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
||||
import {
|
||||
asDateTimestampMs,
|
||||
isFutureDateTimestampMs,
|
||||
resolveDateTimestampMs,
|
||||
resolveExpiresAtMsFromDurationMs,
|
||||
} from "../shared/number-coercion.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { recordTalkObservabilityEvent } from "../talk/observability.js";
|
||||
import {
|
||||
@@ -99,8 +105,10 @@ const handoffs = new Map<string, TalkHandoffRecord>();
|
||||
|
||||
export function createTalkHandoff(params: TalkHandoffCreateParams): TalkHandoffCreateResult {
|
||||
pruneExpiredTalkHandoffs();
|
||||
const createdAt = Date.now();
|
||||
const rawCreatedAt = Date.now();
|
||||
const createdAt = resolveDateTimestampMs(rawCreatedAt);
|
||||
const ttlMs = normalizeTtlMs(params.ttlMs);
|
||||
const expiresAt = resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: rawCreatedAt }) ?? 0;
|
||||
const id = randomUUID();
|
||||
const roomId = `talk_${id}`;
|
||||
const token = randomBytes(32).toString("base64url");
|
||||
@@ -127,7 +135,7 @@ export function createTalkHandoff(params: TalkHandoffCreateParams): TalkHandoffC
|
||||
transport: params.transport ?? "managed-room",
|
||||
brain: params.brain ?? "agent-consult",
|
||||
createdAt,
|
||||
expiresAt: createdAt + ttlMs,
|
||||
expiresAt,
|
||||
room,
|
||||
};
|
||||
appendTalkHandoffRoomEvent(record, {
|
||||
@@ -285,8 +293,12 @@ function normalizeTtlMs(value: number | undefined): number {
|
||||
}
|
||||
|
||||
function pruneExpiredTalkHandoffs(now = Date.now()): void {
|
||||
const validNow = asDateTimestampMs(now);
|
||||
if (validNow === undefined) {
|
||||
return;
|
||||
}
|
||||
for (const [id, record] of handoffs) {
|
||||
if (record.expiresAt <= now) {
|
||||
if (!isFutureDateTimestampMs(record.expiresAt, { nowMs: validNow })) {
|
||||
appendTalkHandoffRoomEvent(record, {
|
||||
type: "session.closed",
|
||||
payload: { reason: "expired", handoffId: id, roomId: record.roomId },
|
||||
@@ -344,7 +356,7 @@ function resolveTalkHandoffAccess(
|
||||
if (!record) {
|
||||
return { ok: false, reason: "not_found" };
|
||||
}
|
||||
if (record.expiresAt <= Date.now()) {
|
||||
if (!isFutureDateTimestampMs(record.expiresAt)) {
|
||||
appendTalkHandoffRoomEvent(record, {
|
||||
type: "session.closed",
|
||||
payload: { reason: "expired", handoffId: id, roomId: record.roomId },
|
||||
|
||||
Reference in New Issue
Block a user