From 8d8583a06be522fbf873955be8024f5a8fcd00c7 Mon Sep 17 00:00:00 2001 From: lukesalamone Date: Fri, 8 May 2026 17:26:54 -0700 Subject: [PATCH 1/3] integrate openai moderation endpoint --- src/worker/comments.ts | 11 ++++++++- src/worker/index.ts | 22 +++++++++++++++++ src/worker/moderation.ts | 52 ++++++++++++++++++++++++++++++++++++++++ wrangler.toml | 5 ++++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/worker/comments.ts b/src/worker/comments.ts index 91a89c9..baae4d1 100644 --- a/src/worker/comments.ts +++ b/src/worker/comments.ts @@ -7,7 +7,7 @@ import { } from "./identity"; import { slugify } from "./slug"; import { rateLimit, clientIp } from "./ratelimit"; -import { moderateCommentNow } from "./moderation"; +import { moderateCommentNow, openaiModerate } from "./moderation"; export interface CommentsEnv { DB: D1Database; @@ -16,6 +16,7 @@ export interface CommentsEnv { OPENROUTER_MODEL: string; OPENROUTER_MODERATION_MODEL?: string; IDENT_PER_IP_PER_HOUR?: string; + OPENAI_API_KEY?: string; } const COOKIE_NAME = "hu_uid"; @@ -421,6 +422,14 @@ export function createCommentsApp() { return c.json({ error: `comment exceeds ${MAX_BODY_LEN} chars` }, 400); } + // Pre-save moderation + if (c.env.OPENAI_API_KEY) { + const flagged = await openaiModerate(body, c.env.OPENAI_API_KEY); + if (flagged) { + return c.json({ error: "comment was flagged by moderation" }, 400); + } + } + if (parent_id) { const parent = await c.env.DB .prepare("SELECT id, slug FROM comments WHERE id = ?") diff --git a/src/worker/index.ts b/src/worker/index.ts index 5326b23..6878583 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -14,9 +14,11 @@ import { isLikelyVpn } from "./vpn"; import { isPermanentlyBlockedSlug } from "./blocklist"; import { loadHints, saveHints } from "./hints"; import { + banSlugNow, countRecentBansByIp, enqueueArticleForModeration, isSlugBanned, + openaiModerate, runSweep, } from "./moderation"; @@ -40,6 +42,9 @@ export interface Env { SEARCH_PER_IP_PER_HOUR?: string; // Single Durable Object tracking live readers per slug. See presence.ts. PRESENCE: DurableObjectNamespace; + // Optional: OpenAI API key for synchronous pre-generation moderation. + // Set via: pnpm wrangler secret put OPENAI_API_KEY + OPENAI_API_KEY?: string; } interface StoredArticle { @@ -554,6 +559,23 @@ app.get("/api/page/:slug", async (c) => { const title = slugToTitle(slug); + // Check title against OpenAI Moderation API before spending LLM tokens + if (c.env.OPENAI_API_KEY) { + const flagged = await openaiModerate(title, c.env.OPENAI_API_KEY); + if (flagged) { + c.executionCtx.waitUntil( + banSlugNow(slug, c.env).catch((e) => + console.error("banSlugNow failed", e) + ) + ); + return c.json( + { error: "this entry has been removed by moderation", banned: true }, + 404, + { "x-robots-tag": "noindex" } + ); + } + } + // Pull every prior link-context blurb other articles have written about // this slug. These become CANON the LLM must respect. let priorHints: string[] = []; diff --git a/src/worker/moderation.ts b/src/worker/moderation.ts index df77ae1..40368fe 100644 --- a/src/worker/moderation.ts +++ b/src/worker/moderation.ts @@ -21,6 +21,58 @@ export interface ModerationEnv { OPENROUTER_API_KEY: string; OPENROUTER_MODEL: string; OPENROUTER_MODERATION_MODEL?: string; + OPENAI_API_KEY?: string; +} + +/** + * Call the OpenAI Moderation API and return true if the text was flagged. + * If this fails, the async sweep can still catch stragglers. + */ +export async function openaiModerate( + text: string, + apiKey: string +): Promise { + try { + const res = await fetch("https://api.openai.com/v1/moderations", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ model: "omni-moderation-latest", input: text }), + }); + if (!res.ok) return false; + const json: any = await res.json(); + return json?.results?.[0]?.flagged === true; + } catch { + return false; + } +} + +/** + * Immediately mark slug as banned in DB and delete it from KV. + * Used when the pre-check flags a slug before generation starts. + */ +export async function banSlugNow( + slug: string, + env: ModerationEnv +): Promise { + const now = Date.now(); + try { + await env.ARTICLES.delete(slug); + } catch {} + try { + await env.DB + .prepare( + `INSERT INTO article_moderation (slug, status, reason, enqueued_at, checked_at) + VALUES (?, 'banned', ?, ?, ?) + ON CONFLICT(slug) DO UPDATE SET status='banned', reason=excluded.reason, checked_at=excluded.checked_at` + ) + .bind(slug, "openai-moderation-precheck", now, now) + .run(); + } catch (e) { + console.error("banSlugNow: DB write failed", slug, e); + } } const BATCH_SIZE = 30; diff --git a/wrangler.toml b/wrangler.toml index 2bc1686..7a8cdc6 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -56,3 +56,8 @@ class_name = "PresenceDO" [[migrations]] tag = "v1" new_sqlite_classes = ["PresenceDO"] +# Enable OpenAI Moderation API for slugs and comments +# pnpm wrangler secret put OPENAI_API_KEY + +# Enable OpenAI Moderation API for slugs and comments +# pnpm wrangler secret put OPENAI_API_KEY From 008a4035e837c0e01e230a83324e394617dfc0b8 Mon Sep 17 00:00:00 2001 From: lynncoleart Date: Thu, 14 May 2026 04:24:13 -0500 Subject: [PATCH 2/3] Fix PR 17 CodeRabbit blockers for comment moderation flow --- src/worker/comments.ts | 16 ++++++++-------- src/worker/moderation.ts | 8 ++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/worker/comments.ts b/src/worker/comments.ts index baae4d1..3e54271 100644 --- a/src/worker/comments.ts +++ b/src/worker/comments.ts @@ -422,14 +422,6 @@ export function createCommentsApp() { return c.json({ error: `comment exceeds ${MAX_BODY_LEN} chars` }, 400); } - // Pre-save moderation - if (c.env.OPENAI_API_KEY) { - const flagged = await openaiModerate(body, c.env.OPENAI_API_KEY); - if (flagged) { - return c.json({ error: "comment was flagged by moderation" }, 400); - } - } - if (parent_id) { const parent = await c.env.DB .prepare("SELECT id, slug FROM comments WHERE id = ?") @@ -452,6 +444,14 @@ export function createCommentsApp() { throw e; } + // Pre-save moderation + if (c.env.OPENAI_API_KEY) { + const flagged = await openaiModerate(body, c.env.OPENAI_API_KEY); + if (flagged) { + return c.json({ error: "comment was flagged by moderation" }, 400); + } + } + const id = crypto.randomUUID(); const created_at = Date.now(); diff --git a/src/worker/moderation.ts b/src/worker/moderation.ts index 40368fe..efafe24 100644 --- a/src/worker/moderation.ts +++ b/src/worker/moderation.ts @@ -32,9 +32,15 @@ export async function openaiModerate( text: string, apiKey: string ): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => { + controller.abort(); + }, 5000); + try { const res = await fetch("https://api.openai.com/v1/moderations", { method: "POST", + signal: controller.signal, headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, @@ -46,6 +52,8 @@ export async function openaiModerate( return json?.results?.[0]?.flagged === true; } catch { return false; + } finally { + clearTimeout(timeout); } } From 2adbdb53abb39c9b57c9a080147b3b59853e858f Mon Sep 17 00:00:00 2001 From: lynncoleart Date: Thu, 14 May 2026 04:27:34 -0500 Subject: [PATCH 3/3] refine openai moderation timeout for comment precheck --- src/worker/moderation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worker/moderation.ts b/src/worker/moderation.ts index efafe24..3c464f6 100644 --- a/src/worker/moderation.ts +++ b/src/worker/moderation.ts @@ -35,7 +35,7 @@ export async function openaiModerate( const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(); - }, 5000); + }, 4000); try { const res = await fetch("https://api.openai.com/v1/moderations", {