{"record":{"id":"325610b657999960","repo":"danny-avila/LibreChat","slug":"invalid-role-id-principalid-325610","errorCode":null,"errorMessage":"Invalid role ID: ${principalId}","messagePattern":"Invalid role ID: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/api/src/acl/accessControlService.ts","lineNumber":73,"sourceCode":"      accessRoleId,\n      grantedBy,\n      session,\n      expiredAt,\n    } = args;\n    try {\n      if (!Object.values(PrincipalType).includes(principalType)) {\n        throw new Error(`Invalid principal type: ${principalType}`);\n      }\n\n      if (principalType !== PrincipalType.PUBLIC && !principalId) {\n        throw new Error('Principal ID is required for user, group, and role principals');\n      }\n\n      // Validate principalId based on type\n      if (principalId && principalType === PrincipalType.ROLE) {\n        // Role IDs are strings (role names)\n        if (typeof principalId !== 'string' || principalId.trim().length === 0) {\n          throw new Error(`Invalid role ID: ${principalId}`);\n        }\n      } else if (\n        principalType &&\n        principalType !== PrincipalType.PUBLIC &&\n        (!principalId || !Types.ObjectId.isValid(principalId))\n      ) {\n        // User and Group IDs must be valid ObjectIds\n        throw new Error(`Invalid principal ID: ${principalId}`);\n      }\n\n      if (!resourceId || !Types.ObjectId.isValid(resourceId)) {\n        throw new Error(`Invalid resource ID: ${resourceId}`);\n      }\n\n      this.validateResourceType(resourceType as ResourceType);\n\n      // Get the role to determine permission bits\n      const role = await this._dbMethods.findRoleByIdentifier(accessRoleId);","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/danny-avila/LibreChat/blob/5ff282f9006c436e561de1afd39a481bea1ef0d8/packages/api/src/acl/accessControlService.ts#L55-L91","documentation":"When principalType is ROLE, grantPermission expects principalId to be a non-empty trimmed string (the role name). This branch rejects role ids that are not strings or are whitespace-only. Role principals are keyed by name, not ObjectId, unlike USER and GROUP.","triggerScenarios":"grantPermission with principalType: PrincipalType.ROLE where principalId is a number, an ObjectId, undefined, or a blank string; passing a role document's _id instead of its identifier string.","commonSituations":"Confusing the role's Mongo _id with its logical identifier (AccessRoleIds); copy-paste from a USER grant that supplies an ObjectId; trimming logic upstream that produced an empty string.","solutions":["Pass the role's logical identifier string (e.g. from AccessRoleIds or the role.name field), not its ObjectId.","Trim and assert non-empty before calling grantPermission.","Double-check principalType is genuinely ROLE; for USER/GROUP an ObjectId is correct."],"exampleFix":"// before\nawait grantPermission({ principalType: PrincipalType.ROLE, principalId: roleDoc._id, ... });\n\n// after\nawait grantPermission({ principalType: PrincipalType.ROLE, principalId: AccessRoleIds.MY_ROLE, ... });","handlingStrategy":"validation","validationCode":"function assertRoleId(v: unknown): string {\n  if (typeof v !== 'string' || v.trim().length === 0) {\n    throw new Error(`Invalid role ID: ${String(v)}`);\n  }\n  return v;\n}","typeGuard":"const isRoleId = (v: unknown): v is string =>\n  typeof v === 'string' && v.trim().length > 0;","tryCatchPattern":null,"preventionTips":["For ROLE principals pass the logical role name (AccessRoleIds), never an ObjectId.","Trim and assert non-empty at the call site."],"tags":["acl","validation","permissions","roles"],"backgroundTag":null,"analyzedSha":"5ff282f9006c436e561de1afd39a481bea1ef0d8","analyzedAt":"2026-08-12T21:38:08.145Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}