lobehub/lobehub · error · TRPCError
FORBIDDEN
FORBIDDEN
Error message
Topic comment resource not found
What it means
Permission gate at the entry of assertTopicCommentReadAccess. The caller lacks TOPIC_COMMENT_READ with either 'all' or 'owner' scope in this workspace, so the read is denied before the topic is even queried. hideExistence defaults to FORBIDDEN, but callers that want to hide resource existence (e.g. unauthenticated shared-link readers) pass hideExistence: true to downgrade to NOT_FOUND.
Source
Thrown at apps/server/src/routers/lambda/_helpers/topicCommentAccess.ts:26
import { assertCanViewTopicTargets } from './conversationResourceGuard';
export const assertTopicCommentReadAccess = async (params: {
db: LobeChatDatabase;
grantedPermissions?: readonly string[];
hideExistence?: boolean;
topicId: string;
userId: string;
workspaceId: string;
}) => {
const permissions = await getWorkspaceScopedPermissionMatches({
action: 'TOPIC_COMMENT_READ',
db: params.db,
grantedPermissions: params.grantedPermissions,
userId: params.userId,
workspaceId: params.workspaceId,
});
if (!permissions.hasAllScope && !permissions.hasOwnerScope) {
throw new TRPCError({
code: params.hideExistence ? 'NOT_FOUND' : 'FORBIDDEN',
message: 'Topic comment resource not found',
});
}
const [topic] = await params.db
.select({ id: topics.id })
.from(topics)
.where(and(eq(topics.id, params.topicId), eq(topics.workspaceId, params.workspaceId)))
.limit(1);
if (!topic) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Topic comment resource not found' });
}
try {
await assertCanViewTopicTargets(
{
db: params.db,View on GitHub (pinned to 10f24d7ade)
Solutions
- Grant the user TOPIC_COMMENT_READ (all or owner scope) for the workspace via the workspace permission admin.
- Confirm the caller's role — viewers may need promotion to member.
- If the caller is the topic creator and should inherently have read access, verify the permission resolution path includes creator-scope fallback.
Example fix
// before: caller has no TOPIC_COMMENT_READ grant
await trpc.topicComment.list.query({ topicId, workspaceId }); // FORBIDDEN
// after: admin grants the permission
await admin.grantWorkspacePermission(userId, workspaceId, 'TOPIC_COMMENT_READ', 'all'); Defensive patterns
Strategy: validation
Validate before calling
const canReadComments = (grantedPermissions: readonly string[] | undefined) =>
grantedPermissions?.some((p) => p.includes('TOPIC_COMMENT_READ')) ?? false;
if (!canReadComments(session.grantedPermissions)) {
// hide the comment UI instead of letting the call fail
} Type guard
null
Try / catch
try {
await trpc.topicComment.list.query({ topicId, workspaceId });
} catch (e) {
if (e.code === 'FORBIDDEN') hideCommentUI();
} Prevention
- Drive the comment UI's visibility from the resolved permission set, not from role name.
- Re-resolve permissions after a role change.
- Cache grantedPermissions per session and refresh on workspace switch.
When it happens
Trigger: Calling a topic-comment read procedure (list/get comments on a topic) when the workspace caller's grantedPermissions do not include a TOPIC_COMMENT_READ grant with workspace-wide or owner scope. Triggered by viewers/members without the comment-read permission, or by callers from a different workspace.
Common situations: A workspace viewer (read-only role) trying to read comments when only members have the grant; permission grants not yet provisioned after a workspace role change; a stale permission cache after a role downgrade.
Related errors
AI-assisted analysis of lobehub/lobehub@10f24d7ade (2026-08-12).
Data as JSON: /api/errors/c8d86219b835eab3.
Report an issue: GitHub.