{"record":{"id":"f0e3cd2fd5e71b8a","repo":"danny-avila/LibreChat","slug":"invalid-role-id-principalid","errorCode":null,"errorMessage":"Invalid role ID: ${principalId}","messagePattern":"Invalid role ID: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"api/server/services/PermissionService.js","lineNumber":88,"sourceCode":"  resourceId,\n  accessRoleId,\n  grantedBy,\n  session,\n}) => {\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      !mongoose.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 || !mongoose.Types.ObjectId.isValid(resourceId)) {\n      throw new Error(`Invalid resource ID: ${resourceId}`);\n    }\n\n    validateResourceType(resourceType);\n\n    // Get the role to determine permission bits\n    const role = await db.findRoleByIdentifier(accessRoleId);","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/danny-avila/LibreChat/blob/5ff282f9006c436e561de1afd39a481bea1ef0d8/api/server/services/PermissionService.js#L70-L106","documentation":"grantPermission() in PermissionService.js:88 throws this when principalType is ROLE and principalId is present but is not a non-empty string. Role principals use the role name as their identifier (not an ObjectId), so the service validates that it is a non-empty trimmed string.","triggerScenarios":"A caller passes principalType=ROLE with a principalId that is a number, an empty string, whitespace-only, null coerced to '', or an ObjectId. The role-name contract is violated.","commonSituations":"Frontend reused the user/group ObjectId field for a role grant. A form defaulted principalId to '' for role selection. Confusion between accessRoleId (the permission level) and the role principalId (the role name).","solutions":["For ROLE principals, pass the role name string (e.g. SystemRoles.ADMIN) as principalId.","Separate the role-name field from the accessRoleId field in the calling form.","Trim and validate non-empty on the client before submitting."],"exampleFix":"// before\ngrantPermission({ principalType: PrincipalType.ROLE, principalId: '  ', ... });\n// after\ngrantPermission({ principalType: PrincipalType.ROLE, principalId: roleName.trim(), ... });","handlingStrategy":"validation","validationCode":"function assertRolePrincipalId(principalId) {\n  if (typeof principalId !== 'string' || principalId.trim().length === 0) {\n    throw new Error('ROLE principalId must be a non-empty role-name string');\n  }\n}","typeGuard":"const isRoleName = (id) => typeof id === 'string' && id.trim().length > 0;","tryCatchPattern":null,"preventionTips":["Keep the role-name field visually distinct from ObjectId fields in the UI.","Populate ROLE principalId from a known role list, never free-form text."],"tags":["permissions","acl","roles","validation"],"backgroundTag":null,"analyzedSha":"5ff282f9006c436e561de1afd39a481bea1ef0d8","analyzedAt":"2026-08-12T21:38:08.145Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}