mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix: clarify publisher abuse ban emails (#2697)
This commit is contained in:
@@ -96,6 +96,38 @@ describe("moderation notification email copy", () => {
|
||||
expect(email.html).not.toContain("fixed local copy");
|
||||
});
|
||||
|
||||
it("builds publisher-abuse account-suspended copy from a structured manual reason", async () => {
|
||||
const email = await buildBanNotificationEmail({
|
||||
handle: "bulkpub",
|
||||
source: "manual",
|
||||
reason: "publisher_abuse: high catalog volume, low installs per skill, abnormal downloads",
|
||||
hiddenArtifacts: 42,
|
||||
});
|
||||
|
||||
expect(email.subject).toBe("Your ClawHub account has been suspended");
|
||||
expect(email.context).toMatchObject({
|
||||
appealUrl: APPEALS_URL,
|
||||
artifact: null,
|
||||
scannerLabel: null,
|
||||
findingSummary:
|
||||
"Your account was identified by ClawHub's publisher abuse review workflow for activity that appears inconsistent with our Acceptable Usage policy.",
|
||||
});
|
||||
expect(email.text).toContain("Hi bulkpub,");
|
||||
expect(email.text).toContain("Bulk or spam publishing");
|
||||
expect(email.text).toContain("Artificially inflating installs, downloads, stars");
|
||||
expect(email.text).toContain(
|
||||
"Abnormal download activity with little or no corresponding install activity",
|
||||
);
|
||||
expect(email.text).toContain("Artifacts hidden");
|
||||
expect(email.text).not.toContain("publisher_abuse:");
|
||||
expect(email.text).not.toContain(`Appeal: ${APPEALS_URL}`);
|
||||
expect(email.html).toContain("Submit an appeal");
|
||||
expect(email.html).toContain("Bulk or spam publishing");
|
||||
expect(email.html).toContain("Artificially inflating installs, downloads, stars");
|
||||
expect(email.html).toContain("Artifacts hidden");
|
||||
expect(email.html).not.toContain("publisher_abuse:");
|
||||
});
|
||||
|
||||
it("builds restored-account copy that explains tokens stay revoked", async () => {
|
||||
const email = await buildRestoredAccountEmail({
|
||||
handle: "restored",
|
||||
|
||||
+38
-8
@@ -5,6 +5,15 @@ export const MALICIOUS_REJECTION_ACCOUNT_WARNING =
|
||||
const MAX_EMAIL_FINDING_SUMMARY_LENGTH = 280;
|
||||
export const ADMIN_ONE_OFF_TEMPLATE = "generic-one-off";
|
||||
|
||||
const PUBLISHER_ABUSE_FINDING_SUMMARY =
|
||||
"Your account was identified by ClawHub's publisher abuse review workflow for activity that appears inconsistent with our Acceptable Usage policy.";
|
||||
const PUBLISHER_ABUSE_POLICY_ITEMS = [
|
||||
"Bulk or spam publishing of large numbers of low-effort, duplicative, placeholder, or machine-generated listings.",
|
||||
"Publishing large catalogs with little or no usage, maintenance, source clarity, or meaningful differentiation.",
|
||||
"Artificially inflating installs, downloads, stars, or other engagement metrics.",
|
||||
"Abnormal download activity with little or no corresponding install activity.",
|
||||
];
|
||||
|
||||
export type NotificationArtifact = {
|
||||
kind: "skill" | "plugin";
|
||||
name: string;
|
||||
@@ -27,6 +36,7 @@ export type BanNotificationEmailContext = {
|
||||
artifact: NotificationArtifact | null;
|
||||
scannerLabel: string | null;
|
||||
findingSummary: string;
|
||||
policyReasonItems: string[];
|
||||
};
|
||||
|
||||
export type TransactionalEmail = {
|
||||
@@ -87,6 +97,8 @@ export type AdminOneOffEmailArgs = {
|
||||
type BanReasonSummary = {
|
||||
scannerLabel: string | null;
|
||||
findingSummary: string;
|
||||
policyReasonItems?: string[];
|
||||
omitTextAppealUrl?: boolean;
|
||||
};
|
||||
|
||||
function normalizeReasonInput(args: Pick<BanNotificationEmailArgs, "reason" | "trigger">) {
|
||||
@@ -132,6 +144,15 @@ function summarizeBanReason(args: BanNotificationEmailArgs): BanReasonSummary {
|
||||
};
|
||||
}
|
||||
|
||||
if (/\bpublisher[_\-\s]?abuse\b/.test(normalized)) {
|
||||
return {
|
||||
scannerLabel: null,
|
||||
findingSummary: PUBLISHER_ABUSE_FINDING_SUMMARY,
|
||||
policyReasonItems: PUBLISHER_ABUSE_POLICY_ITEMS,
|
||||
omitTextAppealUrl: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scannerLabel: null,
|
||||
findingSummary: "ClawHub staff disabled the account after a security review.",
|
||||
@@ -164,6 +185,7 @@ async function renderAccountSuspendedTemplate(args: {
|
||||
suspendedAt?: number;
|
||||
hiddenArtifacts?: number;
|
||||
findingSummary: string;
|
||||
policyReasonItems: string[];
|
||||
preheader: string;
|
||||
}) {
|
||||
const { renderAccountSuspendedEmail } = await import("./emailRendering");
|
||||
@@ -176,6 +198,7 @@ async function renderAccountSuspendedTemplate(args: {
|
||||
suspendedAt: formatUtcTimestamp(args.suspendedAt, "moderation review"),
|
||||
...(hiddenArtifacts === undefined ? {} : { hiddenArtifacts }),
|
||||
findingSummary: args.findingSummary,
|
||||
policyReasonItems: args.policyReasonItems,
|
||||
preheader: args.preheader,
|
||||
});
|
||||
return rendered.html;
|
||||
@@ -242,11 +265,19 @@ export async function buildBanNotificationEmail(
|
||||
): Promise<TransactionalEmail> {
|
||||
const summary = summarizeBanReason(args);
|
||||
const artifact = args.artifact ?? null;
|
||||
const policyReasonItems = summary.policyReasonItems ?? [];
|
||||
const hiddenArtifacts =
|
||||
typeof args.hiddenArtifacts === "number" && Number.isFinite(args.hiddenArtifacts)
|
||||
? Math.max(0, Math.trunc(args.hiddenArtifacts))
|
||||
: artifact
|
||||
? 1
|
||||
: undefined;
|
||||
const context: BanNotificationEmailContext = {
|
||||
appealUrl: APPEALS_URL,
|
||||
artifact,
|
||||
scannerLabel: summary.scannerLabel,
|
||||
findingSummary: summary.findingSummary,
|
||||
policyReasonItems,
|
||||
};
|
||||
|
||||
const lines = [
|
||||
@@ -256,6 +287,10 @@ export async function buildBanNotificationEmail(
|
||||
`Reason: ${context.findingSummary}`,
|
||||
];
|
||||
if (artifact) lines.push(artifactLabel(artifact));
|
||||
if (typeof hiddenArtifacts === "number") lines.push(`Artifacts hidden: ${hiddenArtifacts}`);
|
||||
if (policyReasonItems.length > 0) {
|
||||
lines.push("", "Policy signals:", ...policyReasonItems.map((item) => `- ${item}`));
|
||||
}
|
||||
|
||||
lines.push(
|
||||
"",
|
||||
@@ -263,9 +298,8 @@ export async function buildBanNotificationEmail(
|
||||
"- Your ClawHub account cannot sign in.",
|
||||
"- Existing API tokens for the account have been revoked.",
|
||||
"- Published listings owned by the account may be hidden from public view.",
|
||||
"",
|
||||
`Appeal: ${APPEALS_URL}`,
|
||||
);
|
||||
if (!summary.omitTextAppealUrl) lines.push("", `Appeal: ${APPEALS_URL}`);
|
||||
|
||||
lines.push("", "ClawHub Security");
|
||||
|
||||
@@ -276,20 +310,16 @@ export async function buildBanNotificationEmail(
|
||||
];
|
||||
const detailLines = [
|
||||
context.findingSummary,
|
||||
...policyReasonItems,
|
||||
...(artifact ? [artifact.name] : []),
|
||||
...impactItems,
|
||||
];
|
||||
const hiddenArtifacts =
|
||||
typeof args.hiddenArtifacts === "number" && Number.isFinite(args.hiddenArtifacts)
|
||||
? args.hiddenArtifacts
|
||||
: artifact
|
||||
? 1
|
||||
: undefined;
|
||||
const html = await renderAccountSuspendedTemplate({
|
||||
handle: args.handle,
|
||||
suspendedAt: args.bannedAt,
|
||||
hiddenArtifacts,
|
||||
findingSummary: context.findingSummary,
|
||||
policyReasonItems,
|
||||
preheader: detailLines.join(" "),
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export type AccountSuspendedEmailProps = {
|
||||
suspendedAt: string;
|
||||
hiddenArtifacts?: number;
|
||||
findingSummary: string;
|
||||
policyReasonItems?: string[];
|
||||
preheader: string;
|
||||
};
|
||||
|
||||
@@ -23,6 +24,7 @@ export default function AccountSuspendedEmail({
|
||||
suspendedAt,
|
||||
hiddenArtifacts,
|
||||
findingSummary,
|
||||
policyReasonItems = [],
|
||||
preheader,
|
||||
}: AccountSuspendedEmailProps) {
|
||||
const rows: Array<[string, ReactNode]> = [
|
||||
@@ -45,7 +47,11 @@ export default function AccountSuspendedEmail({
|
||||
Your ClawHub account <MonoPill>{handle}</MonoPill> was suspended after moderation review.
|
||||
</Paragraph>
|
||||
<Paragraph>{findingSummary}</Paragraph>
|
||||
<ImpactList
|
||||
{policyReasonItems.length > 0 ? (
|
||||
<DetailList title="Policy signals" items={policyReasonItems} />
|
||||
) : null}
|
||||
<DetailList
|
||||
title="What changed"
|
||||
items={[
|
||||
"Your ClawHub account cannot sign in.",
|
||||
"Existing API tokens for the account have been revoked.",
|
||||
@@ -59,7 +65,7 @@ export default function AccountSuspendedEmail({
|
||||
);
|
||||
}
|
||||
|
||||
function ImpactList({ items }: { items: string[] }) {
|
||||
function DetailList({ title, items }: { title: string; items: string[] }) {
|
||||
return (
|
||||
<>
|
||||
<h2
|
||||
@@ -70,7 +76,7 @@ function ImpactList({ items }: { items: string[] }) {
|
||||
color: "#f5f5f5",
|
||||
}}
|
||||
>
|
||||
What changed
|
||||
{title}
|
||||
</h2>
|
||||
<ul
|
||||
style={{
|
||||
@@ -93,10 +99,17 @@ function ImpactList({ items }: { items: string[] }) {
|
||||
}
|
||||
|
||||
AccountSuspendedEmail.PreviewProps = {
|
||||
handle: "@octocat",
|
||||
suspendedAt: "2026-06-11 21:32 UTC",
|
||||
hiddenArtifacts: 14,
|
||||
findingSummary: "ClawScan classified the uploaded skill as malicious.",
|
||||
handle: "@bulkpub",
|
||||
suspendedAt: "2026-06-16 17:36 UTC",
|
||||
hiddenArtifacts: 42,
|
||||
findingSummary:
|
||||
"Your account was identified by ClawHub's publisher abuse review workflow for activity that appears inconsistent with our Acceptable Usage policy.",
|
||||
policyReasonItems: [
|
||||
"Bulk or spam publishing of large numbers of low-effort, duplicative, placeholder, or machine-generated listings.",
|
||||
"Publishing large catalogs with little or no usage, maintenance, source clarity, or meaningful differentiation.",
|
||||
"Artificially inflating installs, downloads, stars, or other engagement metrics.",
|
||||
"Abnormal download activity with little or no corresponding install activity.",
|
||||
],
|
||||
preheader:
|
||||
"Your account has been suspended. Login is blocked, API tokens were revoked, and published artifacts may be hidden.",
|
||||
"Your account has been suspended after publisher abuse review. Login is blocked, API tokens were revoked, and published artifacts may be hidden.",
|
||||
} satisfies AccountSuspendedEmailProps;
|
||||
|
||||
Reference in New Issue
Block a user