Skip to content

Commit 7d1f572

Browse files
committed
fix(website): validate prompt API metadata
1 parent 37abd8d commit 7d1f572

2 files changed

Lines changed: 69 additions & 2 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { afterEach, describe, expect, mock, test } from "bun:test";
2+
import { fetchCliMediaPromptItems } from "./prompt-library";
3+
4+
const originalFetch = globalThis.fetch;
5+
6+
afterEach(() => {
7+
globalThis.fetch = originalFetch;
8+
});
9+
10+
function apiItem(overrides: Record<string, unknown> = {}) {
11+
return {
12+
artifact: { alt: "Result", kind: "image", url: "https://cdn.example.com/result.png" },
13+
category: "image",
14+
model: "gpt-image-2",
15+
output: { label: { en: "Image" }, ratio: "4:5" },
16+
prompt: "Create a product portrait.",
17+
slug: "product-portrait",
18+
source: { label: "Example", platform: "External", url: "https://example.com/source" },
19+
summary: { en: "A product portrait prompt." },
20+
tags: ["product"],
21+
title: { en: "Product portrait" },
22+
updatedAt: "2026-09-07",
23+
...overrides,
24+
};
25+
}
26+
27+
function mockPromptApi(items: unknown[]) {
28+
globalThis.fetch = mock(async () =>
29+
new Response(JSON.stringify({ data: { items } }), {
30+
headers: { "content-type": "application/json" },
31+
status: 200,
32+
})) as typeof fetch;
33+
}
34+
35+
describe("prompt library API normalization", () => {
36+
test("preserves explicit upstream output ratios", async () => {
37+
mockPromptApi([apiItem()]);
38+
39+
const items = await fetchCliMediaPromptItems("image");
40+
41+
expect(items).toHaveLength(1);
42+
expect(items[0]?.output.ratio).toBe("4:5");
43+
});
44+
45+
test("rejects non-http source URLs before they can reach links", async () => {
46+
mockPromptApi([
47+
apiItem({ slug: "unsafe-source", source: { label: "Unsafe", platform: "External", url: "javascript:alert(1)" } }),
48+
apiItem({ slug: "safe-source" }),
49+
]);
50+
51+
const items = await fetchCliMediaPromptItems("image");
52+
53+
expect(items.map((item) => item.slug)).toEqual(["safe-source"]);
54+
expect(items[0]?.source.url).toBe("https://example.com/source");
55+
});
56+
});

website/src/lib/prompt-library.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,18 +1261,29 @@ function promptSource(value: unknown, sourcePlatform = "", sourceUrl = ""): Prom
12611261
capturedAt: String(record.captured_at || record.capturedAt || today),
12621262
label: String(record.label || sourcePlatform || "External"),
12631263
platform: ["GitHub", "Social", "Official docs", "Flatkey generated", "Local migration", "External"].includes(platform) ? platform : "External",
1264-
url: String(record.url || sourceUrl || ""),
1264+
url: safeHttpUrl(record.url || sourceUrl),
12651265
};
12661266
}
12671267

1268+
function safeHttpUrl(value: unknown): string {
1269+
const rawUrl = String(value || "").trim();
1270+
if (!rawUrl) return "";
1271+
try {
1272+
const parsed = new URL(rawUrl);
1273+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.toString() : "";
1274+
} catch {
1275+
return "";
1276+
}
1277+
}
1278+
12681279
function isOwnedPromptSource(source: PromptSource): boolean {
12691280
const value = `${source.platform} ${source.label}`.toLowerCase();
12701281
return source.platform === "Local migration" || value.includes("flatkey generated") || value.includes("owned") || value.includes("自有");
12711282
}
12721283

12731284
function outputRatio(value: unknown, artifact: PromptArtifact): PromptItem["output"]["ratio"] {
12741285
const ratio = isRecord(value) ? String(value.ratio || "") : "";
1275-
if (["1:1", "3:2", "4:3", "9:16", "16:9", "3x3"].includes(ratio)) return ratio as PromptItem["output"]["ratio"];
1286+
if (ratio) return ratio as PromptItem["output"]["ratio"];
12761287
return artifact.kind === "video" ? "16:9" : "1:1";
12771288
}
12781289

0 commit comments

Comments
 (0)