fix(zalo): bound hosted media expiry clocks

This commit is contained in:
Peter Steinberger
2026-05-30 11:29:27 -04:00
parent 84385898ec
commit f91ddefbfb
2 changed files with 67 additions and 3 deletions

View File

@@ -151,6 +151,50 @@ describe("zalo outbound hosted media", () => {
expect(secondResponse.res.statusCode).toBe(404);
});
it("rejects hosted media preparation when the expiry would exceed a valid Date", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(8_640_000_000_000_000));
try {
await expect(
prepareHostedZaloMediaUrl({
mediaUrl: "https://example.com/photo.png",
webhookUrl: "https://gateway.example.com/zalo-webhook",
maxBytes: 1024,
}),
).rejects.toThrow(/expiry/);
expect(loadOutboundMediaFromUrlMock).not.toHaveBeenCalled();
} finally {
vi.useRealTimers();
}
});
it("does not serve hosted media when the current clock is invalid", async () => {
const hostedUrl = await prepareHostedZaloMediaUrl({
mediaUrl: "https://example.com/photo.png",
webhookUrl: "https://gateway.example.com/zalo-webhook",
maxBytes: 1024,
});
const { pathname, search } = new URL(hostedUrl);
const response = createMockResponse();
const dateNow = vi.spyOn(Date, "now").mockReturnValue(Number.NaN);
try {
const handled = await tryHandleHostedZaloMediaRequest(
{
method: "GET",
url: `${pathname}${search}`,
} as never,
response.res as never,
);
expect(handled).toBe(true);
expect(response.res.statusCode).toBe(410);
expect(response.res.end).toHaveBeenCalledWith("Expired");
} finally {
dateNow.mockRestore();
}
});
it("rejects hosted media requests with the wrong token", async () => {
const hostedUrl = await prepareHostedZaloMediaUrl({
mediaUrl: "https://example.com/photo.png",

View File

@@ -3,6 +3,10 @@ import { rmSync } from "node:fs";
import { readdir, readFile, stat, unlink } from "node:fs/promises";
import type { IncomingMessage, ServerResponse } from "node:http";
import { join } from "node:path";
import {
asDateTimestampMs,
resolveExpiresAtMsFromDurationMs,
} from "openclaw/plugin-sdk/number-runtime";
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
import { privateFileStore } from "openclaw/plugin-sdk/security-runtime";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
@@ -64,6 +68,10 @@ async function deleteHostedZaloMediaEntry(id: string): Promise<void> {
}
async function cleanupExpiredHostedZaloMedia(nowMs = Date.now()): Promise<void> {
const now = asDateTimestampMs(nowMs);
if (now === undefined) {
return;
}
let fileNames: string[];
try {
fileNames = await readdir(ZALO_OUTBOUND_MEDIA_DIR);
@@ -79,7 +87,8 @@ async function cleanupExpiredHostedZaloMedia(nowMs = Date.now()): Promise<void>
try {
const metadataRaw = await readFile(resolveHostedZaloMediaMetadataPath(id), "utf8");
const metadata = JSON.parse(metadataRaw) as HostedZaloMediaMetadata;
if (metadata.expiresAt <= nowMs) {
const expiresAt = asDateTimestampMs(metadata.expiresAt);
if (expiresAt === undefined || expiresAt <= now) {
await deleteHostedZaloMediaEntry(id);
}
} catch {
@@ -141,6 +150,15 @@ export async function prepareHostedZaloMediaUrl(params: {
await ensureHostedZaloMediaDir();
await cleanupExpiredHostedZaloMedia();
const now = asDateTimestampMs(Date.now());
const expiresAt =
now === undefined
? undefined
: resolveExpiresAtMsFromDurationMs(ZALO_OUTBOUND_MEDIA_TTL_MS, { nowMs: now });
if (expiresAt === undefined) {
throw new Error("Zalo outbound media expiry could not be resolved");
}
const media = await loadOutboundMediaFromUrl(params.mediaUrl, {
maxBytes: params.maxBytes,
...(params.proxyUrl ? { proxyUrl: params.proxyUrl } : {}),
@@ -161,7 +179,7 @@ export async function prepareHostedZaloMediaUrl(params: {
routePath,
token,
contentType: media.contentType,
expiresAt: Date.now() + ZALO_OUTBOUND_MEDIA_TTL_MS,
expiresAt,
} satisfies HostedZaloMediaMetadata);
} catch (error) {
await deleteHostedZaloMediaEntry(id);
@@ -210,7 +228,9 @@ export async function tryHandleHostedZaloMediaRequest(
return true;
}
if (entry.metadata.expiresAt <= Date.now()) {
const now = asDateTimestampMs(Date.now());
const expiresAt = asDateTimestampMs(entry.metadata.expiresAt);
if (now === undefined || expiresAt === undefined || expiresAt <= now) {
await deleteHostedZaloMediaEntry(id);
res.statusCode = 410;
res.end("Expired");