{"record":{"id":"b7be6699a1273807","repo":"hcengineering/platform","slug":"platform-status-forbidden-b7be66","errorCode":"platform.status.Forbidden","errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"exception","errorClass":"PlatformError","httpStatus":null,"severity":"error","filePath":"server/account/src/operations.ts","lineNumber":1096,"sourceCode":" * Given an invite and a token, checks if the user has already joined the workspace and updates the role if necessary.\n * Returns the workspace login information if the user has already joined. Otherwise, throws an error.\n */\nexport async function checkJoin (\n  ctx: MeasureContext,\n  db: AccountDB,\n  branding: Branding | null,\n  token: string,\n  params: { inviteId: string }\n): Promise<WorkspaceLoginInfo> {\n  const { inviteId } = params\n\n  if (inviteId == null || inviteId === '') {\n    throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))\n  }\n\n  const invite = await getWorkspaceInvite(db, inviteId)\n  if (invite == null) {\n    throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))\n  }\n\n  const { account: accountUuid } = decodeTokenVerbose(ctx, token)\n  const emailSocialId = await db.socialId.findOne({\n    type: SocialIdType.EMAIL,\n    personUuid: accountUuid,\n    verifiedOn: { $gt: 0 }\n  })\n  const email = emailSocialId?.value ?? ''\n  const workspaceUuid = await checkInvite(ctx, invite, email)\n  const workspace = await getWorkspaceById(db, workspaceUuid)\n\n  if (workspace === null) {\n    ctx.error('Workspace not found in checkJoin', { workspaceUuid, email, inviteId })\n    throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))\n  }\n\n  const role = await db.getWorkspaceRole(accountUuid, workspace.uuid)","sourceCodeStart":1078,"sourceCodeEnd":1114,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/server/account/src/operations.ts#L1078-L1114","documentation":"checkJoin looks up the invitation via getWorkspaceInvite(db, inviteId); when no invite exists for that id it throws platform.status.Forbidden. The server intentionally returns Forbidden rather than NotFound so it does not leak which invite ids are valid.","triggerScenarios":"Calling checkJoin with an inviteId whose invite row was deleted (invite revoked or expired cleanup job); a typo'd or fabricated inviteId; an invite from a different database/environment.","commonSituations":"Users clicking stale invite links after the admin revoked them; invites pruned by retention/expiry jobs; testing with inviteIds copied from another environment; invites invalidated by a workspace deletion that left links circulating.","solutions":["Request a fresh invite link from a workspace admin, since the referenced invite no longer exists.","Verify the inviteId matches an entry in the workspace invite store (getWorkspaceInvite) before calling.","Confirm you are calling the same environment where the invite was created.","Handle this status in the UI with an 'invite is no longer valid' message rather than retrying."],"exampleFix":"// before\nawait ops.checkJoin(ctx, token, { inviteId }) // invite revoked\n// after\nconst invite = await getWorkspaceInvite(db, inviteId)\nif (!invite) return { error: 'invite-no-longer-valid', requestNewInvite: true }\nawait ops.checkJoin(ctx, token, { inviteId })","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  return await ops.checkJoin(ctx, token, { inviteId })\n} catch (e) {\n  if (isStatus(e, platform.status.Forbidden)) {\n    // treat as unknown/revoked invite: offer 'request new invite' UX\n    return null\n  }\n  throw e\n}","preventionTips":["Do not cache invite links; always fetch the latest one from the workspace admin API.","Give invites expiry handling in the UI (warn before the retention window ends).","Never guess or fabricate inviteIds."],"tags":["invite","forbidden","not-found"],"backgroundTag":"invite-invalid-or-expired","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}