toeverything/AFFiNE · error · ExpectToRevokeDocUserRoles

expect_to_revoke_doc_user_roles

expect_to_revoke_doc_user_roles

Error message

Expect doc not to be workspace

What it means

Thrown by the revokeDocUserRole GraphQL mutation when input.docId === input.workspaceId. In this codebase a workspace's root doc shares the workspace's id, so the per-doc grant APIs do not apply to it; access to the root is governed entirely by workspace membership. The guard exists because calling revokeDocUserRole on the workspace root would silently no-op or corrupt workspace-level permissions, so it is rejected up front as invalid_input (HTTP 400).

Source

Thrown at packages/backend/server/src/core/workspaces/resolvers/doc.ts:771

    this.logger.log(`Grant doc user roles (${JSON.stringify(info)})`);
    return true;
  }

  @Mutation(() => Boolean)
  async revokeDocUserRoles(
    @CurrentUser() user: CurrentUser,
    @Args('input') input: RevokeDocUserRoleInput
  ): Promise<boolean> {
    const pairs = {
      spaceId: input.workspaceId,
      docId: input.docId,
    };
    if (input.workspaceId === input.docId) {
      this.logger.error(
        'Expect to revoke doc user roles, but it is a workspace',
        pairs
      );
      throw new ExpectToRevokeDocUserRoles(
        pairs,
        'Expect doc not to be workspace'
      );
    }
    await this.ac.user(user.id).doc(input).assert('Doc.Users.Manage');

    await this.models.docUser.delete(
      input.workspaceId,
      input.docId,
      input.userId
    );
    this.event.emit('doc.grants.changed', {
      workspaceId: input.workspaceId,
      docId: input.docId,
    });

    const info = {
      ...pairs,

View on GitHub (pinned to 26c515e050)

Solutions

  1. Pass the nested doc's id as docId and keep the workspace id in workspaceId.
  2. If you intend to remove a workspace member, call revokeMember(workspaceId, userId) instead.
  3. Add a client-side precondition: if (workspaceId === docId) route to the workspace-member flow rather than the doc-grant flow.

Example fix

// before
revokeDocUserRole({ workspaceId: ws.id, docId: ws.id, userId });
// after
revokeDocUserRole({ workspaceId: ws.id, docId: page.id, userId });
Defensive patterns

Strategy: validation

Validate before calling

function assertRevokeDocArgs(input: { workspaceId: string; docId: string }) {
  if (!input.workspaceId || !input.docId) throw new Error('workspaceId and docId are required');
  if (input.workspaceId === input.docId) {
    throw new Error('Cannot revoke doc roles on the workspace root; use revokeMember instead');
  }
}
// call before revokeDocUserRole
assertRevokeDocArgs(input);

Type guard

function isNestedDocInput(input: { workspaceId: string; docId: string }): boolean {
  return Boolean(input.workspaceId) && Boolean(input.docId) && input.workspaceId !== input.docId;
}

Prevention

When it happens

Trigger: Calling mutation revokeDocUserRole with RevokeDocUserRoleInput where docId and workspaceId are the same string value (e.g. passing the workspace id as docId).

Common situations: A frontend 'manage doc access' screen is reused for the workspace root page and fills both fields from the same workspace id; an older client that never split the two ids; copying a workspace id from the URL into both args of a manual GraphQL request.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/57ce89b5f9cb667b. Report an issue: GitHub.