hcengineering/platform · error · PlatformError

platform.status.Forbidden

platform.status.Forbidden

Error message

Forbidden

What it means

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.

Source

Thrown at server/account/src/operations.ts:1096

 * Given an invite and a token, checks if the user has already joined the workspace and updates the role if necessary.
 * Returns the workspace login information if the user has already joined. Otherwise, throws an error.
 */
export async function checkJoin (
  ctx: MeasureContext,
  db: AccountDB,
  branding: Branding | null,
  token: string,
  params: { inviteId: string }
): Promise<WorkspaceLoginInfo> {
  const { inviteId } = params

  if (inviteId == null || inviteId === '') {
    throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
  }

  const invite = await getWorkspaceInvite(db, inviteId)
  if (invite == null) {
    throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
  }

  const { account: accountUuid } = decodeTokenVerbose(ctx, token)
  const emailSocialId = await db.socialId.findOne({
    type: SocialIdType.EMAIL,
    personUuid: accountUuid,
    verifiedOn: { $gt: 0 }
  })
  const email = emailSocialId?.value ?? ''
  const workspaceUuid = await checkInvite(ctx, invite, email)
  const workspace = await getWorkspaceById(db, workspaceUuid)

  if (workspace === null) {
    ctx.error('Workspace not found in checkJoin', { workspaceUuid, email, inviteId })
    throw new PlatformError(new Status(Severity.ERROR, platform.status.WorkspaceNotFound, { workspaceUuid }))
  }

  const role = await db.getWorkspaceRole(accountUuid, workspace.uuid)

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Request a fresh invite link from a workspace admin, since the referenced invite no longer exists.
  2. Verify the inviteId matches an entry in the workspace invite store (getWorkspaceInvite) before calling.
  3. Confirm you are calling the same environment where the invite was created.
  4. Handle this status in the UI with an 'invite is no longer valid' message rather than retrying.

Example fix

// before
await ops.checkJoin(ctx, token, { inviteId }) // invite revoked
// after
const invite = await getWorkspaceInvite(db, inviteId)
if (!invite) return { error: 'invite-no-longer-valid', requestNewInvite: true }
await ops.checkJoin(ctx, token, { inviteId })
Defensive patterns

Strategy: try-catch

Try / catch

try {
  return await ops.checkJoin(ctx, token, { inviteId })
} catch (e) {
  if (isStatus(e, platform.status.Forbidden)) {
    // treat as unknown/revoked invite: offer 'request new invite' UX
    return null
  }
  throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/b7be6699a1273807. Report an issue: GitHub.