Files
shd.build/src/routes/isac/community/+page.server.ts
T

1160 lines
50 KiB
TypeScript

import { avatarUrl } from '$lib/avatar';
import { recordIsacAudit } from '$lib/server/audit';
import { getPermissions, requirePermission } from '$lib/server/auth/authorization';
import { getSessionUser } from '$lib/server/auth/session';
import { getDatabase } from '$lib/server/database';
import { renderUserMarkdown } from '$lib/markdown';
import { createNotification } from '$lib/server/notifications';
import { getPagination, getRegistrySearch, getRegistrySort } from '$lib/server/pagination';
import { error, fail } from '@sveltejs/kit';
import { randomUUID } from 'node:crypto';
import type { Actions, PageServerLoad } from './$types';
import { agentProfileHref } from '$lib/agents';
const COMMENT_PERMISSION = 'community.comments.moderate';
const PROFILE_PERMISSION = 'community.profiles.moderate';
const REMOVED_RETENTION_DAYS = 30;
const has = (permissions: string[], permission: string) =>
permissions.includes('*') || permissions.includes(permission);
const actionFailure = (status: number, message: string) => fail(status, {
communityError: message,
communityResultToken: randomUUID()
});
const requiredReason = (value: FormDataEntryValue | null) => {
const reason = String(value ?? '').normalize('NFKC').trim().slice(0, 500);
return reason.length >= 3 ? reason : null;
};
const targetHref = (targetType: string, targetId: unknown, slug: unknown, publicSlug?: unknown) =>
targetType === 'profile'
? agentProfileHref({
id: String(targetId),
publicSlug: typeof publicSlug === 'string' ? publicSlug : null
}, 'profile-comments')
: `/builds/${encodeURIComponent(String(slug ?? ''))}#comments`;
const parseAuditSnapshot = (value: unknown) => {
if (typeof value !== 'string') return null;
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? parsed as Record<string, unknown>
: null;
} catch {
return null;
}
};
type QueueContentType = 'comment' | 'profile_bio' | 'guide';
type QueueSource = 'scan' | 'report';
const contentPermission = (contentType: QueueContentType) =>
contentType === 'profile_bio' ? PROFILE_PERMISSION : COMMENT_PERMISSION;
const parseJsonObject = (value: unknown) => {
if (typeof value !== 'string') return {};
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? parsed as Record<string, unknown>
: {};
} catch {
return {};
}
};
const parseJsonArray = (value: unknown) => {
if (typeof value !== 'string') return [];
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
};
const queueTargetHref = (
contentType: QueueContentType,
contentId: unknown,
targetType: unknown,
targetId: unknown,
slug: unknown,
publicSlug?: unknown
) => {
if (contentType === 'profile_bio') {
return agentProfileHref({
id: String(contentId),
publicSlug: typeof publicSlug === 'string' ? publicSlug : null
});
}
if (contentType === 'guide') {
return slug
? `/builds/${encodeURIComponent(String(slug))}`
: `/builds/${encodeURIComponent(String(contentId))}`;
}
return targetHref(String(targetType), targetId, slug, publicSlug);
};
const queueActionFailure = (status: number, message: string) => fail(status, {
communityQueueError: message,
communityResultToken: randomUUID()
});
const allowedQueueTypes = (canModerateComments: boolean, canModerateProfiles: boolean) => [
...(canModerateComments ? ['comment', 'guide'] as QueueContentType[] : []),
...(canModerateProfiles ? ['profile_bio'] as QueueContentType[] : [])
];
const assertQueuePermission = async (
operator: NonNullable<Awaited<ReturnType<typeof getSessionUser>>>,
contentType: QueueContentType
) => {
const granted = await getPermissions(operator);
if (!has(granted, contentPermission(contentType))) {
error(403, contentType === 'profile_bio'
? 'Profile moderation permission required'
: 'Comment moderation permission required');
}
};
const scanQueueRecord = async (database: Awaited<ReturnType<typeof getDatabase>>, id: string) =>
(await database.prepare(`SELECT scan.*,revision.body_markdown AS revision_body,
comments.body_markdown AS comment_body,comments.status AS comment_status,
comments.thread_id,thread.target_type,thread.target_id,target_build.slug,target_build.title AS target_title,
target_build.guide_body AS current_guide_body,target_build.visibility AS current_guide_visibility,
target_build.owner_id AS guide_owner_id,
profile.bio_markdown AS current_bio,profile.bio_updated_at AS current_bio_updated_at,
profile.updated_at AS profile_updated_at,COALESCE(profile.display_name,users.username) AS display_name
FROM content_moderation_scans scan
LEFT JOIN content_moderation_reviews review ON review.scan_id=scan.id
LEFT JOIN moderated_content_revisions revision ON revision.id=scan.revision_id
LEFT JOIN comments ON scan.content_type='comment' AND comments.id=scan.content_id
LEFT JOIN comment_threads thread ON thread.id=comments.thread_id
LEFT JOIN builds target_build ON
(scan.content_type='guide' AND target_build.id=scan.content_id)
OR (scan.content_type='comment' AND thread.target_type IN ('build','guide')
AND target_build.id=thread.target_id)
LEFT JOIN user_profiles profile ON profile.user_id=scan.author_id
LEFT JOIN users ON users.id=scan.author_id
WHERE scan.id=? AND scan.content_type IN ('comment','profile_bio','guide')
AND scan.status IN ('review','priority_review','error') AND review.id IS NULL`)).get(id);
const reportQueueRecord = async (database: Awaited<ReturnType<typeof getDatabase>>, id: string) =>
(await database.prepare(`SELECT report.*,
comments.body_markdown AS comment_body,comments.status AS comment_status,
comments.author_id AS comment_author_id,comments.thread_id,
thread.target_type,thread.target_id,target_build.slug,target_build.title AS target_title,
target_build.guide_body AS current_guide_body,target_build.visibility AS current_guide_visibility,
target_build.owner_id AS guide_owner_id,
profile.bio_markdown AS current_bio,profile.is_public AS profile_is_public,
COALESCE(moderation.bio_hidden,0) AS bio_hidden
FROM user_content_reports report
LEFT JOIN comments ON report.content_type='comment' AND comments.id=report.content_id
LEFT JOIN comment_threads thread ON thread.id=comments.thread_id
LEFT JOIN builds target_build ON
(report.content_type='guide' AND target_build.id=report.content_id)
OR (report.content_type='comment' AND thread.target_type IN ('build','guide')
AND target_build.id=thread.target_id)
LEFT JOIN user_profiles profile ON report.content_type='profile_bio'
AND profile.user_id=report.content_id
LEFT JOIN profile_moderation moderation ON moderation.user_id=profile.user_id
WHERE report.id=? AND report.status='pending'
AND report.content_type IN ('comment','profile_bio','guide')`)).get(id);
export const load: PageServerLoad = async ({ cookies, url }) => {
const operator = await getSessionUser(cookies);
if (!operator) error(401, 'Sign in required');
const granted = await getPermissions(operator);
const canModerateComments = has(granted, COMMENT_PERMISSION);
const canModerateProfiles = has(granted, PROFILE_PERMISSION);
if (!canModerateComments && !canModerateProfiles) error(403, 'Community Oversight permission required');
const requestedTab = url.searchParams.get('tab');
const tab = requestedTab === 'queue'
? 'queue'
: requestedTab === 'bios'
? 'bios'
: requestedTab === 'comments'
? 'comments'
: 'queue';
if (tab === 'comments' && !canModerateComments) error(403, 'Comment moderation permission required');
if (tab === 'bios' && !canModerateProfiles) error(403, 'Profile moderation permission required');
const allowedStatuses = tab === 'queue'
? ['all', 'priority', 'automated', 'reported']
: tab === 'comments'
? ['all', 'visible', 'removed']
: ['all', 'visible', 'hidden'];
const status = allowedStatuses.includes(String(url.searchParams.get('status')))
? String(url.searchParams.get('status'))
: tab === 'queue' ? 'all' : 'visible';
const { search, pattern } = getRegistrySearch(url);
const database = await getDatabase();
const removedCutoff = new Date(Date.now() - REMOVED_RETENTION_DAYS * 86_400_000).toISOString();
const commentSummaryRows = canModerateComments
? await (await database.prepare(`SELECT status,COUNT(*) AS count FROM comments
WHERE status='visible' OR (status='removed' AND removed_at>=?)
GROUP BY status`)).all(removedCutoff)
: [];
const visibleBios = canModerateProfiles
? Number((await (await database.prepare(`SELECT COUNT(*) AS count FROM user_profiles profile
LEFT JOIN profile_moderation moderation ON moderation.user_id=profile.user_id
WHERE profile.is_public=1 AND COALESCE(moderation.bio_hidden,0)=0
AND length(trim(profile.bio_markdown))>0`)).get())?.count ?? 0)
: 0;
const hiddenBios = canModerateProfiles
? Number((await (await database.prepare(`SELECT COUNT(*) AS count FROM user_profiles profile
JOIN profile_moderation moderation ON moderation.user_id=profile.user_id
WHERE moderation.bio_hidden=1 AND length(trim(profile.bio_markdown))>0`)).get())?.count ?? 0)
: 0;
const commentSummary = Object.fromEntries(
commentSummaryRows.map((row) => [String(row.status), Number(row.count)])
);
const queueTypes = allowedQueueTypes(canModerateComments, canModerateProfiles);
const queueTypePlaceholders = queueTypes.map(() => '?').join(',');
const [scanQueueSummary, reportQueueSummary] = await Promise.all([
(await database.prepare(`SELECT COUNT(*) AS total,
SUM(CASE WHEN status='priority_review' THEN 1 ELSE 0 END) AS priority,
SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS errors
FROM content_moderation_scans scan
WHERE scan.content_type IN (${queueTypePlaceholders})
AND scan.status IN ('review','priority_review','error')
AND NOT EXISTS(
SELECT 1 FROM content_moderation_reviews review WHERE review.scan_id=scan.id
)`)).get(...queueTypes),
(await database.prepare(`SELECT COUNT(*) AS total
FROM user_content_reports
WHERE content_type IN (${queueTypePlaceholders}) AND status='pending'`)).get(...queueTypes)
]);
const queueSummary = {
automated: Number(scanQueueSummary?.total ?? 0),
reported: Number(reportQueueSummary?.total ?? 0),
priority: Number(scanQueueSummary?.priority ?? 0) + Number(scanQueueSummary?.errors ?? 0)
};
const summary = {
queue: { ...queueSummary, total: queueSummary.automated + queueSummary.reported },
comments: { visible: Number(commentSummary.visible ?? 0), removed: Number(commentSummary.removed ?? 0) },
bios: { visible: visibleBios, hidden: hiddenBios }
};
if (tab === 'queue') {
const scanQueue = `SELECT
scan.id AS queue_id,'scan' AS item_source,scan.content_type,scan.content_id,
scan.author_id,COALESCE(author_profile.display_name,author.username,'Deleted Agent') AS display_name,
author_profile.public_slug AS author_public_slug,
author_avatar.source AS avatar_source,author_avatar.value AS avatar_value,
author_avatar.updated_at AS avatar_updated_at,
COALESCE(revision.body_markdown,comments.body_markdown,target_build.guide_body,'') AS body_markdown,
scan.created_at AS queued_at,scan.status AS scan_status,scan.risk_level,scan.reason,
NULL AS category,scan.provider,scan.model,scan.provider_request_id,
scan.scores_json,scan.flags_json,scan.rule_matches_json,NULL AS reporter_name,
NULL AS details,thread.target_type,thread.target_id,
CASE
WHEN scan.content_type='profile_bio'
THEN COALESCE(author_profile.display_name,author.username,'Agent profile')
WHEN scan.content_type='guide'
THEN COALESCE(target_build.title,'Build guide')
ELSE COALESCE(target_build.title,target_profile.display_name,target_user.username,'Community content')
END AS target_title,
target_build.slug,COALESCE(target_profile.public_slug,author_profile.public_slug) AS target_public_slug
FROM content_moderation_scans scan
LEFT JOIN content_moderation_reviews review ON review.scan_id=scan.id
LEFT JOIN moderated_content_revisions revision ON revision.id=scan.revision_id
LEFT JOIN comments ON scan.content_type='comment' AND comments.id=scan.content_id
LEFT JOIN comment_threads thread ON thread.id=comments.thread_id
LEFT JOIN builds target_build ON
(scan.content_type='guide' AND target_build.id=scan.content_id)
OR (scan.content_type='comment' AND thread.target_type IN ('build','guide')
AND target_build.id=thread.target_id)
LEFT JOIN users target_user ON scan.content_type='comment'
AND thread.target_type='profile' AND target_user.id=thread.target_id
LEFT JOIN user_profiles target_profile ON target_profile.user_id=target_user.id
LEFT JOIN users author ON author.id=scan.author_id
LEFT JOIN user_profiles author_profile ON author_profile.user_id=author.id
LEFT JOIN user_avatars author_avatar ON author_avatar.user_id=author.id
WHERE scan.content_type IN (${queueTypePlaceholders})
AND scan.status IN ('review','priority_review','error') AND review.id IS NULL`;
const reportQueue = `SELECT
report.id AS queue_id,'report' AS item_source,report.content_type,report.content_id,
COALESCE(comments.author_id,profile.user_id,target_build.owner_id) AS author_id,
COALESCE(author_profile.display_name,author.username,'Deleted Agent') AS display_name,
author_profile.public_slug AS author_public_slug,
author_avatar.source AS avatar_source,author_avatar.value AS avatar_value,
author_avatar.updated_at AS avatar_updated_at,
COALESCE(comments.body_markdown,profile.bio_markdown,target_build.guide_body,'') AS body_markdown,
report.created_at AS queued_at,NULL AS scan_status,'reported' AS risk_level,
COALESCE(report.details,'A community member requested Oversight review.') AS reason,
report.category,NULL AS provider,NULL AS model,NULL AS provider_request_id,
'{}' AS scores_json,'{}' AS flags_json,'[]' AS rule_matches_json,
COALESCE(reporter_profile.display_name,reporter.username,'Community member') AS reporter_name,
report.details,thread.target_type,thread.target_id,
CASE
WHEN report.content_type='profile_bio'
THEN COALESCE(profile.display_name,author.username,'Agent profile')
WHEN report.content_type='guide'
THEN COALESCE(target_build.title,'Build guide')
ELSE COALESCE(target_build.title,target_profile.display_name,target_user.username,'Community content')
END AS target_title,
target_build.slug,COALESCE(target_profile.public_slug,profile.public_slug) AS target_public_slug
FROM user_content_reports report
LEFT JOIN comments ON report.content_type='comment' AND comments.id=report.content_id
LEFT JOIN comment_threads thread ON thread.id=comments.thread_id
LEFT JOIN user_profiles profile ON report.content_type='profile_bio'
AND profile.user_id=report.content_id
LEFT JOIN builds target_build ON
(report.content_type='guide' AND target_build.id=report.content_id)
OR (report.content_type='comment' AND thread.target_type IN ('build','guide')
AND target_build.id=thread.target_id)
LEFT JOIN users target_user ON report.content_type='comment'
AND thread.target_type='profile' AND target_user.id=thread.target_id
LEFT JOIN user_profiles target_profile ON target_profile.user_id=target_user.id
LEFT JOIN users author ON author.id=COALESCE(comments.author_id,profile.user_id,target_build.owner_id)
LEFT JOIN user_profiles author_profile ON author_profile.user_id=author.id
LEFT JOIN user_avatars author_avatar ON author_avatar.user_id=author.id
LEFT JOIN users reporter ON reporter.id=report.reporter_id
LEFT JOIN user_profiles reporter_profile ON reporter_profile.user_id=reporter.id
WHERE report.content_type IN (${queueTypePlaceholders}) AND report.status='pending'`;
const union = status === 'automated'
? scanQueue
: status === 'reported'
? reportQueue
: `${scanQueue} UNION ALL ${reportQueue}`;
const args: unknown[] = status === 'automated' || status === 'reported'
? [...queueTypes]
: [...queueTypes, ...queueTypes];
const conditions: string[] = [];
if (status === 'priority') {
conditions.push("item_source='scan' AND scan_status IN ('priority_review','error')");
}
if (search) {
conditions.push(`(body_markdown LIKE ? ESCAPE '\\' COLLATE NOCASE
OR display_name LIKE ? ESCAPE '\\' COLLATE NOCASE
OR target_title LIKE ? ESCAPE '\\' COLLATE NOCASE
OR COALESCE(reason,'') LIKE ? ESCAPE '\\' COLLATE NOCASE
OR COALESCE(category,'') LIKE ? ESCAPE '\\' COLLATE NOCASE)`);
args.push(pattern, pattern, pattern, pattern, pattern);
}
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const total = Number((await (await database.prepare(
`WITH queue_items AS (${union}) SELECT COUNT(*) AS count FROM queue_items ${where}`
)).get(...args))?.count ?? 0);
const pagination = getPagination(url, total);
const rows = await (await database.prepare(`WITH queue_items AS (${union})
SELECT * FROM queue_items ${where}
ORDER BY CASE
WHEN scan_status='priority_review' THEN 0
WHEN scan_status='error' THEN 1
WHEN item_source='report' THEN 2
ELSE 3
END,queued_at DESC LIMIT ? OFFSET ?`))
.all(...args, pagination.perPage, pagination.offset);
return {
tab,
status,
search,
summary,
items: rows.map((row) => {
const contentType = String(row.content_type) as QueueContentType;
const scores = parseJsonObject(row.scores_json);
const flags = parseJsonObject(row.flags_json);
return {
id: String(row.queue_id),
source: String(row.item_source) as QueueSource,
contentType,
contentId: String(row.content_id),
bodyHtml: renderUserMarkdown(row.body_markdown),
queuedAt: String(row.queued_at),
scanStatus: row.scan_status ? String(row.scan_status) : null,
riskLevel: String(row.risk_level),
reason: String(row.reason),
category: row.category ? String(row.category) : null,
provider: row.provider ? String(row.provider) : null,
model: row.model ? String(row.model) : null,
providerRequestId: row.provider_request_id ? String(row.provider_request_id) : null,
reporterName: row.reporter_name ? String(row.reporter_name) : null,
scores: Object.entries(scores)
.map(([category, score]) => ({ category, score: Number(score) }))
.filter((entry) => Number.isFinite(entry.score) && entry.score > 0)
.sort((left, right) => right.score - left.score)
.slice(0, 5),
flags: Object.entries(flags)
.filter(([, flagged]) => Boolean(flagged))
.map(([category]) => category),
ruleMatches: parseJsonArray(row.rule_matches_json).flatMap((match) => {
if (!match || typeof match !== 'object' || Array.isArray(match)) return [];
const record = match as Record<string, unknown>;
return [{
label: String(record.label ?? record.kind ?? 'LOCAL RULE'),
excerpt: String(record.excerpt ?? '')
}];
}),
targetType: contentType === 'comment' ? String(row.target_type ?? 'content') : contentType,
targetTitle: String(row.target_title),
targetHref: queueTargetHref(
contentType,
row.content_id,
row.target_type,
row.target_id,
row.slug,
row.target_public_slug
),
author: {
id: String(row.author_id ?? ''),
displayName: String(row.display_name),
publicSlug: row.author_public_slug ? String(row.author_public_slug) : null,
avatarUrl: avatarUrl(
row.avatar_source,
row.avatar_value,
String(row.author_id ?? ''),
row.avatar_updated_at
)
}
};
}),
pagination,
permissions: { comments: canModerateComments, profiles: canModerateProfiles }
};
}
if (tab === 'comments') {
const sort = getRegistrySort(url, {
created: {
asc: 'comments.created_at ASC',
desc: 'comments.created_at DESC',
defaultDirection: 'desc'
},
updated: {
asc: 'comments.updated_at ASC',
desc: 'comments.updated_at DESC',
defaultDirection: 'desc'
},
author: {
asc: 'display_name COLLATE NOCASE ASC',
desc: 'display_name COLLATE NOCASE DESC'
}
}, 'created', 'desc');
const conditions: string[] = [];
const args: unknown[] = [];
if (status === 'all') {
conditions.push("(comments.status='visible' OR (comments.status='removed' AND comments.removed_at>=?))");
args.push(removedCutoff);
} else {
conditions.push('comments.status=?');
args.push(status);
}
if (status === 'removed') {
conditions.push('comments.removed_at>=?');
args.push(removedCutoff);
}
if (search) {
conditions.push(`(comments.body_markdown LIKE ? ESCAPE '\\' COLLATE NOCASE
OR COALESCE(profile.display_name,users.username,'Deleted Agent') LIKE ? ESCAPE '\\' COLLATE NOCASE
OR COALESCE(builds.title,target_profile.display_name,target_user.username,'') LIKE ? ESCAPE '\\' COLLATE NOCASE)`);
args.push(pattern, pattern, pattern);
}
const joins = `FROM comments
JOIN comment_threads thread ON thread.id=comments.thread_id
LEFT JOIN users ON users.id=comments.author_id
LEFT JOIN user_profiles profile ON profile.user_id=users.id
LEFT JOIN user_avatars avatar ON avatar.user_id=users.id
LEFT JOIN builds ON builds.id=thread.target_id AND thread.target_type IN ('build','guide')
LEFT JOIN users target_user ON target_user.id=thread.target_id AND thread.target_type='profile'
LEFT JOIN user_profiles target_profile ON target_profile.user_id=target_user.id`;
const where = `WHERE ${conditions.join(' AND ')}`;
const total = Number((await (await database.prepare(
`SELECT COUNT(*) AS count ${joins} ${where}`
)).get(...args))?.count ?? 0);
const pagination = getPagination(url, total);
const rows = await (await database.prepare(`SELECT comments.id,comments.body_markdown,comments.status,
comments.created_at,comments.updated_at,comments.removal_reason,
comments.author_id,COALESCE(profile.display_name,users.username,'Deleted Agent') AS display_name,
profile.public_slug AS author_public_slug,target_profile.public_slug AS target_public_slug,
avatar.source AS avatar_source,avatar.value AS avatar_value,avatar.updated_at AS avatar_updated_at,
thread.target_type,thread.target_id,builds.slug,
COALESCE(builds.title,target_profile.display_name,target_user.username,'Community content') AS target_title
${joins} ${where} ORDER BY ${sort.orderBy} LIMIT ? OFFSET ?`))
.all(...args, pagination.perPage, pagination.offset);
return {
tab,
status,
search,
summary,
items: rows.map((row) => ({
id: String(row.id),
bodyHtml: renderUserMarkdown(row.body_markdown),
status: String(row.status),
createdAt: String(row.created_at),
updatedAt: String(row.updated_at),
moderationReason: row.removal_reason ? String(row.removal_reason) : null,
targetType: String(row.target_type),
targetTitle: String(row.target_title),
targetHref: targetHref(String(row.target_type), row.target_id, row.slug, row.target_public_slug),
author: {
id: String(row.author_id),
displayName: String(row.display_name),
publicSlug: row.author_public_slug ? String(row.author_public_slug) : null,
avatarUrl: avatarUrl(row.avatar_source, row.avatar_value, String(row.author_id), row.avatar_updated_at)
}
})),
pagination,
sort: { key: sort.key, direction: sort.direction },
permissions: { comments: canModerateComments, profiles: canModerateProfiles }
};
}
const sort = getRegistrySort(url, {
updated: {
asc: 'profile.updated_at ASC',
desc: 'profile.updated_at DESC',
defaultDirection: 'desc'
},
agent: {
asc: 'display_name COLLATE NOCASE ASC',
desc: 'display_name COLLATE NOCASE DESC'
}
}, 'updated', 'desc');
const conditions = [
status === 'all'
? "((profile.is_public=1 AND COALESCE(moderation.bio_hidden,0)=0) OR moderation.bio_hidden=1)"
: status === 'visible'
? 'profile.is_public=1 AND COALESCE(moderation.bio_hidden,0)=0'
: 'moderation.bio_hidden=1',
'length(trim(profile.bio_markdown))>0'
];
const args: unknown[] = [];
if (search) {
conditions.push(`(profile.bio_markdown LIKE ? ESCAPE '\\' COLLATE NOCASE
OR COALESCE(profile.display_name,users.username) LIKE ? ESCAPE '\\' COLLATE NOCASE)`);
args.push(pattern, pattern);
}
const where = `WHERE ${conditions.join(' AND ')}`;
const total = Number((await (await database.prepare(`SELECT COUNT(*) AS count
FROM user_profiles profile JOIN users ON users.id=profile.user_id
LEFT JOIN profile_moderation moderation ON moderation.user_id=profile.user_id
${where}`)).get(...args))?.count ?? 0);
const pagination = getPagination(url, total);
const rows = await (await database.prepare(`SELECT profile.user_id,profile.public_slug,profile.bio_markdown,profile.is_public,
profile.updated_at,COALESCE(profile.display_name,users.username) AS display_name,
avatar.source AS avatar_source,avatar.value AS avatar_value,avatar.updated_at AS avatar_updated_at,
moderation.reason AS moderation_reason
FROM user_profiles profile
JOIN users ON users.id=profile.user_id
LEFT JOIN user_avatars avatar ON avatar.user_id=users.id
LEFT JOIN profile_moderation moderation ON moderation.user_id=users.id
${where} ORDER BY ${sort.orderBy} LIMIT ? OFFSET ?`))
.all(...args, pagination.perPage, pagination.offset);
return {
tab,
status,
search,
summary,
items: rows.map((row) => ({
id: String(row.user_id),
bodyHtml: renderUserMarkdown(row.bio_markdown),
status,
updatedAt: String(row.updated_at),
moderationReason: row.moderation_reason ? String(row.moderation_reason) : null,
agent: {
id: String(row.user_id),
displayName: String(row.display_name),
publicSlug: row.public_slug ? String(row.public_slug) : null,
avatarUrl: avatarUrl(row.avatar_source, row.avatar_value, String(row.user_id), row.avatar_updated_at)
}
})),
pagination,
sort: { key: sort.key, direction: sort.direction },
permissions: { comments: canModerateComments, profiles: canModerateProfiles }
};
};
export const actions: Actions = {
approveQueue: async ({ cookies, request }) => {
const operator = await getSessionUser(cookies);
if (!operator) return queueActionFailure(401, 'Sign in to review community content.');
const form = await request.formData();
const id = String(form.get('id') ?? '').trim();
const source = String(form.get('source') ?? '') as QueueSource;
const reason = requiredReason(form.get('reason'));
if (!id || !['scan', 'report'].includes(source) || !reason) {
return queueActionFailure(400, 'Choose a queue item and provide a brief approval reason.');
}
const database = await getDatabase();
if (source === 'report') {
const report = await reportQueueRecord(database, id);
if (!report) return queueActionFailure(404, 'That user report is no longer pending.');
const contentType = String(report.content_type) as QueueContentType;
await assertQueuePermission(operator, contentType);
const now = new Date().toISOString();
await database.exec('BEGIN IMMEDIATE');
try {
const result = await (await database.prepare(`UPDATE user_content_reports
SET status='dismissed',reviewed_at=?,reviewed_by=?,resolution_reason=?
WHERE id=? AND status='pending'`)).run(now, operator.id, reason, id);
if (!result.changes) {
await database.exec('ROLLBACK');
return queueActionFailure(409, 'That report changed before it could be dismissed.');
}
await recordIsacAudit({
actorId: operator.id,
category: 'community_moderation',
action: 'content_report.dismissed',
entityId: id,
before: {
status: 'pending',
contentType,
contentId: String(report.content_id),
category: String(report.category),
reporterId: String(report.reporter_id)
},
after: {
status: 'dismissed',
reason,
reviewedAt: now,
reviewedBy: operator.id
}
});
await database.exec('COMMIT');
} catch (cause) {
await database.exec('ROLLBACK');
throw cause;
}
return { communitySaved: true, communityResultToken: randomUUID() };
}
const scan = await scanQueueRecord(database, id);
if (!scan) return queueActionFailure(404, 'That automated review is no longer pending.');
const contentType = String(scan.content_type) as QueueContentType;
await assertQueuePermission(operator, contentType);
const body = contentType === 'comment' ? String(scan.comment_body ?? '') : String(scan.revision_body ?? '');
if (!body.trim()) return queueActionFailure(409, 'The queued content is no longer available.');
const now = new Date().toISOString();
const reviewId = randomUUID();
let targetOwnerId: string | null = null;
await database.exec('BEGIN IMMEDIATE');
try {
const claim = await (await database.prepare(`INSERT INTO content_moderation_reviews
(id,scan_id,reviewer_id,decision,reason,created_at)
SELECT ?,?,?,'approved',?,?
WHERE NOT EXISTS(SELECT 1 FROM content_moderation_reviews WHERE scan_id=?)`))
.run(reviewId, id, operator.id, reason, now, id);
if (!claim.changes) {
await database.exec('ROLLBACK');
return queueActionFailure(409, 'Another reviewer already handled this scan.');
}
if (contentType === 'comment') {
const result = await (await database.prepare(`UPDATE comments
SET status='visible',removed_at=NULL,removed_by=NULL,removal_reason=NULL,updated_at=?
WHERE id=? AND status='pending_review'`)).run(now, scan.content_id);
if (!result.changes) throw new Error('The pending comment changed before approval.');
targetOwnerId = String(scan.target_type) === 'profile'
? String(scan.target_id)
: scan.guide_owner_id ? String(scan.guide_owner_id) : null;
} else if (contentType === 'profile_bio') {
await (await database.prepare(`INSERT INTO user_profiles
(user_id,display_name,bio_markdown,is_public,bio_updated_at,updated_at)
VALUES(?,?,?,1,?,?)
ON CONFLICT(user_id) DO UPDATE SET
bio_markdown=excluded.bio_markdown,
bio_updated_at=excluded.bio_updated_at,
updated_at=excluded.updated_at`))
.run(scan.content_id, String(scan.display_name ?? 'Agent'), body, now, now);
await (await database.prepare(`UPDATE profile_moderation SET bio_hidden=0,
reason=?,moderated_at=?,moderated_by=? WHERE user_id=?`))
.run(reason, now, operator.id, scan.content_id);
} else {
const result = await (await database.prepare(`UPDATE builds
SET guide_body=?,updated_at=? WHERE id=? AND owner_id=?`))
.run(body, now, scan.content_id, scan.author_id);
if (!result.changes) throw new Error('The queued guide changed before approval.');
}
await recordIsacAudit({
actorId: operator.id,
category: 'community_moderation',
action: `${contentType}.review_approved`,
entityId: String(scan.content_id),
before: {
scanId: id,
status: String(scan.status),
riskLevel: String(scan.risk_level),
reason: String(scan.reason),
policyVersion: Number(scan.policy_version),
provider: String(scan.provider),
model: String(scan.model),
currentStatus: contentType === 'comment'
? String(scan.comment_status)
: contentType === 'profile_bio' ? 'revision_pending' : 'revision_pending'
},
after: {
scanId: id,
reviewId,
decision: 'approved',
reason,
reviewedAt: now,
reviewedBy: operator.id,
published: true
}
});
await database.exec('COMMIT');
} catch (cause) {
await database.exec('ROLLBACK');
if (cause instanceof Error && cause.message.includes('changed before approval')) {
return queueActionFailure(409, cause.message);
}
throw cause;
}
const authorId = String(scan.author_id);
const link = queueTargetHref(
contentType,
scan.content_id,
scan.target_type,
scan.target_id,
scan.slug
);
await createNotification({
userId: authorId,
category: 'isac',
source: 'ISAC COMMUNITY OVERSIGHT',
title: contentType === 'comment'
? 'Comment approved'
: contentType === 'profile_bio' ? 'Agent bio approved' : 'Guide approved',
body: `Community Oversight approved your ${contentType === 'profile_bio' ? 'Agent bio' : contentType}. Reason: ${reason}`,
link,
actions: [{ label: 'VIEW CONTENT', href: link }],
mandatory: true
});
if (contentType === 'comment' && targetOwnerId && targetOwnerId !== authorId) {
await createNotification({
userId: targetOwnerId,
category: 'comments',
source: 'agent',
title: 'New comment',
body: `${String(scan.display_name ?? 'An Agent')} posted a comment on your ${String(scan.target_type) === 'profile' ? 'Agent profile' : 'build'}.`,
link,
actions: [{ label: 'VIEW COMMENT', href: link }]
});
}
return { communitySaved: true, communityResultToken: randomUUID() };
},
rejectQueue: async ({ cookies, request }) => {
const operator = await getSessionUser(cookies);
if (!operator) return queueActionFailure(401, 'Sign in to review community content.');
const form = await request.formData();
const id = String(form.get('id') ?? '').trim();
const source = String(form.get('source') ?? '') as QueueSource;
const reason = requiredReason(form.get('reason'));
if (!id || !['scan', 'report'].includes(source) || !reason) {
return queueActionFailure(400, 'Choose a queue item and provide a brief rejection reason.');
}
const database = await getDatabase();
if (source === 'report') {
const report = await reportQueueRecord(database, id);
if (!report) return queueActionFailure(404, 'That user report is no longer pending.');
const contentType = String(report.content_type) as QueueContentType;
await assertQueuePermission(operator, contentType);
const authorId = contentType === 'comment'
? String(report.comment_author_id ?? '')
: contentType === 'profile_bio'
? String(report.content_id)
: String(report.guide_owner_id ?? '');
if (!authorId) return queueActionFailure(409, 'The reported content author is no longer available.');
const now = new Date().toISOString();
const before = {
reportId: id,
contentType,
contentId: String(report.content_id),
reportCategory: String(report.category),
reporterId: String(report.reporter_id),
contentStatus: contentType === 'comment'
? String(report.comment_status ?? 'missing')
: contentType === 'profile_bio'
? Boolean(report.bio_hidden) ? 'hidden' : 'visible'
: String(report.current_guide_visibility ?? 'missing')
};
await database.exec('BEGIN IMMEDIATE');
try {
if (contentType === 'comment') {
const result = await (await database.prepare(`UPDATE comments SET status='removed',
removed_at=?,removed_by=?,removal_reason=?,updated_at=?
WHERE id=? AND status='visible'`))
.run(now, operator.id, reason, now, report.content_id);
if (!result.changes) throw new Error('The reported comment is no longer visible.');
} else if (contentType === 'profile_bio') {
if (!String(report.current_bio ?? '').trim() || Boolean(report.bio_hidden)) {
throw new Error('The reported biography is no longer visible.');
}
await (await database.prepare(`INSERT INTO profile_moderation
(user_id,bio_hidden,reason,moderated_at,moderated_by) VALUES(?,1,?,?,?)
ON CONFLICT(user_id) DO UPDATE SET bio_hidden=1,reason=excluded.reason,
moderated_at=excluded.moderated_at,moderated_by=excluded.moderated_by`))
.run(report.content_id, reason, now, operator.id);
} else {
if (!String(report.current_guide_body ?? '').trim() || report.current_guide_visibility === 'private') {
throw new Error('The reported guide is no longer publicly available.');
}
const result = await (await database.prepare(`UPDATE builds
SET visibility='private',updated_at=? WHERE id=? AND visibility<>'private'`))
.run(now, report.content_id);
if (!result.changes) throw new Error('The reported guide changed before it could be hidden.');
}
const resolved = await (await database.prepare(`UPDATE user_content_reports
SET status='resolved',reviewed_at=?,reviewed_by=?,resolution_reason=?
WHERE content_type=? AND content_id=? AND status='pending'`))
.run(now, operator.id, reason, contentType, report.content_id);
if (!resolved.changes) throw new Error('The pending reports changed before resolution.');
await recordIsacAudit({
actorId: operator.id,
category: 'community_moderation',
action: contentType === 'comment'
? 'comment.report_removed'
: contentType === 'profile_bio' ? 'profile_bio.report_hidden' : 'guide.report_hidden',
entityId: String(report.content_id),
before,
after: {
status: contentType === 'comment' ? 'removed' : 'hidden',
reason,
reviewedAt: now,
reviewedBy: operator.id,
resolvedReports: Number(resolved.changes)
}
});
await database.exec('COMMIT');
} catch (cause) {
await database.exec('ROLLBACK');
if (cause instanceof Error && (
cause.message.includes('no longer')
|| cause.message.includes('changed before')
)) return queueActionFailure(409, cause.message);
throw cause;
}
const link = queueTargetHref(
contentType,
report.content_id,
report.target_type,
report.target_id,
report.slug
);
await createNotification({
userId: authorId,
category: 'isac',
source: 'ISAC COMMUNITY OVERSIGHT',
title: contentType === 'comment'
? 'Comment removed'
: contentType === 'profile_bio' ? 'Agent bio hidden' : 'Guide hidden',
body: `Community Oversight ${contentType === 'comment' ? 'removed' : 'hid'} your ${contentType === 'profile_bio' ? 'Agent bio' : contentType}. Reason: ${reason}`,
link: contentType === 'profile_bio' ? '/account' : link,
actions: [{
label: 'REVIEW CONTENT',
href: contentType === 'profile_bio' ? '/account' : link
}],
mandatory: true
});
return { communitySaved: true, communityResultToken: randomUUID() };
}
const scan = await scanQueueRecord(database, id);
if (!scan) return queueActionFailure(404, 'That automated review is no longer pending.');
const contentType = String(scan.content_type) as QueueContentType;
await assertQueuePermission(operator, contentType);
const now = new Date().toISOString();
const reviewId = randomUUID();
await database.exec('BEGIN IMMEDIATE');
try {
const claim = await (await database.prepare(`INSERT INTO content_moderation_reviews
(id,scan_id,reviewer_id,decision,reason,created_at)
SELECT ?,?,?,'rejected',?,?
WHERE NOT EXISTS(SELECT 1 FROM content_moderation_reviews WHERE scan_id=?)`))
.run(reviewId, id, operator.id, reason, now, id);
if (!claim.changes) {
await database.exec('ROLLBACK');
return queueActionFailure(409, 'Another reviewer already handled this scan.');
}
if (contentType === 'comment') {
const result = await (await database.prepare(`UPDATE comments SET status='removed',
removed_at=?,removed_by=?,removal_reason=?,updated_at=?
WHERE id=? AND status='pending_review'`))
.run(now, operator.id, reason, now, scan.content_id);
if (!result.changes) throw new Error('The pending comment changed before rejection.');
}
await recordIsacAudit({
actorId: operator.id,
category: 'community_moderation',
action: `${contentType}.review_rejected`,
entityId: String(scan.content_id),
before: {
scanId: id,
status: String(scan.status),
riskLevel: String(scan.risk_level),
reason: String(scan.reason),
policyVersion: Number(scan.policy_version),
provider: String(scan.provider),
model: String(scan.model)
},
after: {
scanId: id,
reviewId,
decision: 'rejected',
reason,
reviewedAt: now,
reviewedBy: operator.id,
published: false
}
});
await database.exec('COMMIT');
} catch (cause) {
await database.exec('ROLLBACK');
if (cause instanceof Error && cause.message.includes('changed before rejection')) {
return queueActionFailure(409, cause.message);
}
throw cause;
}
await createNotification({
userId: String(scan.author_id),
category: 'isac',
source: 'ISAC COMMUNITY OVERSIGHT',
title: contentType === 'comment'
? 'Comment rejected'
: contentType === 'profile_bio' ? 'Agent bio revision rejected' : 'Guide revision rejected',
body: `Community Oversight rejected your ${contentType === 'profile_bio' ? 'Agent bio revision' : contentType}. Reason: ${reason}`,
link: '/account',
actions: [{ label: 'REVIEW ACCOUNT', href: '/account' }],
mandatory: true
});
return { communitySaved: true, communityResultToken: randomUUID() };
},
removeComment: async ({ cookies, request }) => {
const operator = await requirePermission(await getSessionUser(cookies), COMMENT_PERMISSION);
const form = await request.formData();
const id = String(form.get('id') ?? '').trim();
const reason = requiredReason(form.get('reason'));
if (!id || !reason) return actionFailure(400, 'Choose a comment and provide a brief removal reason.');
const database = await getDatabase();
const comment = await (await database.prepare(`SELECT comments.*,thread.target_type,thread.target_id,builds.slug
FROM comments JOIN comment_threads thread ON thread.id=comments.thread_id
LEFT JOIN builds ON builds.id=thread.target_id AND thread.target_type IN ('build','guide')
WHERE comments.id=? AND comments.status='visible'`)).get(id);
if (!comment) return actionFailure(404, 'That visible comment no longer exists.');
const before = {
status: 'visible',
bodyMarkdown: String(comment.body_markdown),
authorId: String(comment.author_id),
threadId: String(comment.thread_id),
targetType: String(comment.target_type),
targetId: String(comment.target_id),
updatedAt: String(comment.updated_at)
};
const now = new Date().toISOString();
const result = await (await database.prepare(`UPDATE comments SET status='removed',removed_at=?,
removed_by=?,removal_reason=?,updated_at=? WHERE id=? AND status='visible'`))
.run(now, operator.id, reason, now, id);
if (!result.changes) return actionFailure(409, 'That comment changed before it could be removed.');
const after = { ...before, status: 'removed', removedAt: now, removedBy: operator.id, reason, updatedAt: now };
await recordIsacAudit({
actorId: operator.id,
category: 'community_moderation',
action: 'comment.removed',
entityId: id,
before,
after
});
await createNotification({
userId: String(comment.author_id),
category: 'isac',
source: 'ISAC COMMUNITY OVERSIGHT',
title: 'Comment removed',
body: `Community Oversight removed one of your comments. Reason: ${reason}`,
link: targetHref(String(comment.target_type), comment.target_id, comment.slug),
actions: [{
label: 'VIEW DISCUSSION',
href: targetHref(String(comment.target_type), comment.target_id, comment.slug)
}],
mandatory: true
});
return { communitySaved: true, communityResultToken: randomUUID() };
},
restoreComment: async ({ cookies, request }) => {
const operator = await requirePermission(await getSessionUser(cookies), COMMENT_PERMISSION);
const form = await request.formData();
const id = String(form.get('id') ?? '').trim();
const reason = requiredReason(form.get('reason'));
if (!id || !reason) return actionFailure(400, 'Choose a comment and provide a brief restoration reason.');
const database = await getDatabase();
const comment = await (await database.prepare(`SELECT * FROM comments WHERE id=? AND status='removed'`)).get(id);
if (!comment) return actionFailure(404, 'That removed comment no longer exists.');
const audit = await (await database.prepare(`SELECT after_json FROM audit_log
WHERE entity_type='community_moderation' AND entity_id=? AND action='comment.removed'
ORDER BY created_at DESC LIMIT 1`)).get(id);
const expected = parseAuditSnapshot(audit?.after_json);
if (
!expected
|| expected.status !== 'removed'
|| String(expected.removedAt ?? '') !== String(comment.removed_at ?? '')
|| String(expected.updatedAt ?? '') !== String(comment.updated_at ?? '')
) return actionFailure(409, 'This comment was not removed by Community Oversight or changed after removal.');
const before = {
status: 'removed',
bodyMarkdown: String(comment.body_markdown),
authorId: String(comment.author_id),
threadId: String(comment.thread_id),
removedAt: String(comment.removed_at),
removedBy: String(comment.removed_by),
removalReason: String(comment.removal_reason),
updatedAt: String(comment.updated_at)
};
const now = new Date().toISOString();
const result = await (await database.prepare(`UPDATE comments SET status='visible',removed_at=NULL,
removed_by=NULL,removal_reason=NULL,updated_at=? WHERE id=? AND status='removed' AND updated_at=?`))
.run(now, id, comment.updated_at);
if (!result.changes) return actionFailure(409, 'That comment changed before it could be restored.');
await recordIsacAudit({
actorId: operator.id,
category: 'community_moderation',
action: 'comment.restored',
entityId: id,
before,
after: {
status: 'visible',
bodyMarkdown: String(comment.body_markdown),
authorId: String(comment.author_id),
threadId: String(comment.thread_id),
restorationReason: reason,
updatedAt: now
}
});
return { communitySaved: true, communityResultToken: randomUUID() };
},
hideBio: async ({ cookies, request }) => {
const operator = await requirePermission(await getSessionUser(cookies), PROFILE_PERMISSION);
const form = await request.formData();
const id = String(form.get('id') ?? '').trim();
const reason = requiredReason(form.get('reason'));
if (!id || !reason) return actionFailure(400, 'Choose a profile and provide a brief reason for hiding it.');
const database = await getDatabase();
const profile = await (await database.prepare(`SELECT profile.user_id,profile.bio_markdown,
profile.is_public,profile.bio_updated_at,profile.updated_at,
COALESCE(moderation.bio_hidden,0) AS bio_hidden,moderation.reason,
moderation.moderated_at,moderation.moderated_by
FROM user_profiles profile
LEFT JOIN profile_moderation moderation ON moderation.user_id=profile.user_id
WHERE profile.user_id=? AND profile.is_public=1
AND COALESCE(moderation.bio_hidden,0)=0 AND length(trim(profile.bio_markdown))>0`)).get(id);
if (!profile) return actionFailure(404, 'That visible profile biography no longer exists.');
const before = {
bioMarkdown: String(profile.bio_markdown),
isPublic: true,
bioHidden: false,
bioUpdatedAt: profile.bio_updated_at ? String(profile.bio_updated_at) : null,
profileUpdatedAt: String(profile.updated_at),
moderation: profile.moderated_at ? {
reason: profile.reason ? String(profile.reason) : null,
moderatedAt: String(profile.moderated_at),
moderatedBy: profile.moderated_by ? String(profile.moderated_by) : null
} : null
};
const now = new Date().toISOString();
const result = await (await database.prepare(`INSERT INTO profile_moderation
(user_id,bio_hidden,reason,moderated_at,moderated_by) VALUES(?,1,?,?,?)
ON CONFLICT(user_id) DO UPDATE SET bio_hidden=1,reason=excluded.reason,
moderated_at=excluded.moderated_at,moderated_by=excluded.moderated_by
WHERE profile_moderation.bio_hidden=0`)).run(id, reason, now, operator.id);
if (!result.changes) return actionFailure(409, 'That profile changed before it could be hidden.');
const after = {
...before,
bioHidden: true,
reason,
moderatedAt: now,
moderatedBy: operator.id
};
await recordIsacAudit({
actorId: operator.id,
category: 'community_moderation',
action: 'profile.hidden',
entityId: id,
before,
after
});
await createNotification({
userId: id,
category: 'isac',
source: 'ISAC COMMUNITY OVERSIGHT',
title: 'Agent profile hidden',
body: `Community Oversight hid your public Agent profile. Your biography is preserved but is not publicly visible. Reason: ${reason}`,
link: '/account',
actions: [{ label: 'REVIEW PROFILE', href: '/account' }],
mandatory: true
});
return { communitySaved: true, communityResultToken: randomUUID() };
},
restoreBio: async ({ cookies, request }) => {
const operator = await requirePermission(await getSessionUser(cookies), PROFILE_PERMISSION);
const form = await request.formData();
const id = String(form.get('id') ?? '').trim();
const reason = requiredReason(form.get('reason'));
if (!id || !reason) return actionFailure(400, 'Choose a profile and provide a brief restoration reason.');
const database = await getDatabase();
const profile = await (await database.prepare(`SELECT profile.user_id,profile.bio_markdown,
profile.is_public,profile.bio_updated_at,profile.updated_at,
moderation.bio_hidden,moderation.reason,moderation.moderated_at,moderation.moderated_by
FROM user_profiles profile
JOIN profile_moderation moderation ON moderation.user_id=profile.user_id
WHERE profile.user_id=? AND moderation.bio_hidden=1
AND length(trim(profile.bio_markdown))>0`)).get(id);
if (!profile) return actionFailure(404, 'That hidden profile biography no longer exists.');
const audit = await (await database.prepare(`SELECT after_json FROM audit_log
WHERE entity_type='community_moderation' AND entity_id=? AND action='profile.hidden'
ORDER BY created_at DESC LIMIT 1`)).get(id);
const expected = parseAuditSnapshot(audit?.after_json);
if (
!expected
|| expected.bioHidden !== true
|| String(expected.bioMarkdown ?? '') !== String(profile.bio_markdown)
|| String(expected.bioUpdatedAt ?? '') !== String(profile.bio_updated_at ?? '')
|| String(expected.profileUpdatedAt ?? '') !== String(profile.updated_at)
|| String(expected.moderatedAt ?? '') !== String(profile.moderated_at)
|| String(expected.moderatedBy ?? '') !== String(profile.moderated_by)
) return actionFailure(409, 'This profile was not hidden by Community Oversight or changed after it was hidden.');
const before = {
bioMarkdown: String(profile.bio_markdown),
isPublic: Boolean(profile.is_public),
bioHidden: true,
bioUpdatedAt: profile.bio_updated_at ? String(profile.bio_updated_at) : null,
profileUpdatedAt: String(profile.updated_at),
reason: String(profile.reason),
moderatedAt: String(profile.moderated_at),
moderatedBy: String(profile.moderated_by)
};
const now = new Date().toISOString();
const result = await (await database.prepare(`UPDATE profile_moderation
SET bio_hidden=0,reason=?,moderated_at=?,moderated_by=?
WHERE user_id=? AND bio_hidden=1 AND moderated_at=?`))
.run(reason, now, operator.id, id, profile.moderated_at);
if (!result.changes) return actionFailure(409, 'That profile changed before it could be restored.');
await recordIsacAudit({
actorId: operator.id,
category: 'community_moderation',
action: 'profile.restored',
entityId: id,
before,
after: {
...before,
bioHidden: false,
restorationReason: reason,
moderatedAt: now,
moderatedBy: operator.id
}
});
return { communitySaved: true, communityResultToken: randomUUID() };
}
};