hcengineering/platform · error · PlatformError

account.status.Forbidden

account.status.Forbidden

Error message

Forbidden

What it means

Thrown when removing a social id from an account would leave it in an invalid state. After computing the social ids that would remain ('afterRemoval'), the account service refuses the release if no HULY social id remains or if no login-capable social id remains. This guarantees every account keeps a working login method.

Source

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

    }
  }

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

  if (!allowedService) {
    // User should always have at least one Huly and one "login" social id
    // so do not allow releasing last ones
    const socialIds = await db.socialId.find({ personUuid, verifiedOn: { $gt: 0 }, isDeleted: { $ne: true } })
    const afterRemoval = socialIds.filter((it) => it.type !== type || it.value !== value)

    if (afterRemoval.filter((it) => it.type === SocialIdType.HULY).length === 0) {
      throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
    }

    if (afterRemoval.filter((it) => loginSocialTypes.includes(it.type)).length === 0) {
      throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
    }
  }

  return await doReleaseSocialId(db, personUuid, type, value, extra?.service ?? account, deleteIntegrations)
}

export async function deleteAccount (
  ctx: MeasureContext,
  db: AccountDB,
  branding: Branding | null,
  token: string,
  params: { uuid?: AccountUuid }
): Promise<void> {
  const { extra } = decodeTokenVerbose(ctx, token)

  const isAdmin = extra?.admin === 'true'

  if (!isAdmin) {

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Add a new login-capable social id to the account before removing the current one
  2. Remove a different (non-last) social id instead
  3. If the removal is intentional, first release other ids then re-add a valid login id immediately after in the same administrative flow

Example fix

// before
await client.removeSocialId(token, SocialIdType.GITHUB, value) // account has only this login id
// after
await client.addSocialId(token, SocialIdType.EMAIL, newEmail)
await client.removeSocialId(token, SocialIdType.GITHUB, value)
Defensive patterns

Strategy: validation

Validate before calling

const remaining = socialIds.filter(id => id.value !== removingValue)
const loginTypes = [SocialIdType.HULY, SocialIdType.EMAIL, SocialIdType.GITHUB]
if (remaining.filter(i => i.type === SocialIdType.HULY).length === 0 ||
    remaining.filter(i => loginTypes.includes(i.type)).length === 0) {
  throw new Error('cannot remove: account must keep a HULY id and at least one login id')
}

Type guard

function hasSafeRemaining(ids: SocialId[], removing: SocialId): boolean {
  const rest = ids.filter(i => i._id !== removing._id)
  return rest.some(i => i.type === SocialIdType.HULY) && rest.some(i => LOGIN_TYPES.includes(i.type))
}

Try / catch

try {
  await accountClient.releaseSocialId(token, type, value)
} catch (err) {
  if ((err as PlatformError).props?.code === platform.status.Forbidden) {
    console.error('cannot remove last login social id; add a replacement first')
  } else throw err
}

Prevention

When it happens

Trigger: Calling the release/remove social id operation (doReleaseSocialId path in server/account/src/operations.ts) when it would remove the last HULY-type social id, or the last social id whose type is in the allowed loginSocialTypes list.

Common situations: A user tries to unlink their only Huly login or last email/GitHub identity; scripts cleaning up social integrations delete all ids at once; admin tooling removes a social id without first adding a replacement.

Understand the failure class

Related errors


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