hcengineering/platform · error · PlatformError

BadRequest

BadRequest

Error message

platform.status.BadRequest

What it means

Thrown by the password-setup (requestPasswordSetup) operation when the target account already has a password hash and salt. The setup flow is only for accounts that never had a password (e.g. signup-via-email accounts), because it bypasses the old-password check in changePassword.

Source

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

 * email+password as a secondary sign-in method.
 *
 * Requires authentication (session token). Only valid for accounts that have
 * no password set — accounts with an existing password must use changePassword.
 */
export async function requestPasswordSetup (
  ctx: MeasureContext,
  db: AccountDB,
  branding: Branding | null,
  token: string
): Promise<void> {
  const { account: accountUuid } = decodeTokenVerbose(ctx, token)

  // Guard: reject if the account already has a password. The setup flow
  // bypasses the old-password requirement in changePassword, so it must only
  // be accessible to accounts that have no password yet.
  const existingAccount = await getAccount(db, accountUuid)
  if (existingAccount?.hash != null && existingAccount?.salt != null) {
    throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
  }

  ctx.info('Requesting password setup', { accountUuid })

  const emailSocialId = await db.socialId.findOne({
    type: SocialIdType.EMAIL,
    personUuid: accountUuid
  })

  if (emailSocialId == null) {
    ctx.error('Email social id not found for account', { accountUuid })
    throw new PlatformError(
      new Status(Severity.ERROR, platform.status.SocialIdNotFound, { value: '', type: SocialIdType.EMAIL })
    )
  }

  const { mailURL, mailAuth } = getMailUrl()
  const front = getFrontUrl(branding)

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Route the user to the normal password-reset (restore) flow instead of password setup.
  2. Use changePassword with the existing old password when the account already has credentials.
  3. Invalidate stale setup links/tokens in emails.
  4. Check account.hash/salt client-side if account info is available before requesting setup.

Example fix

// before: always call setup flow
await accountClient.requestPasswordSetup(accountUuid)
// after: only when no password set yet
if (existingAccount?.hash == null && existingAccount?.salt == null) {
  await accountClient.requestPasswordSetup(accountUuid)
} else {
  await accountClient.requestRestore(ctx, email)
}
Defensive patterns

Strategy: validation

Validate before calling

const acct = await getAccount(db, accountUuid)
const needsSetup = acct?.hash == null && acct?.salt == null
if (!needsSetup) throw new Error('Account already has a password; use restore flow')

Type guard

function hasNoPassword(a: { hash?: string | null; salt?: string | null }): boolean { return a.hash == null && a.salt == null }

Try / catch

try {
  await accountClient.requestPasswordSetup(ctx, branding, accountUuid)
} catch (err) {
  if (err instanceof PlatformError && err.status.code === platform.status.BadRequest) {
    await accountClient.requestRestore(ctx, email) // fall back to reset flow
  } else throw err
}

Prevention

When it happens

Trigger: Calling the password-setup request endpoint for an accountUuid whose existingAccount.hash != null && salt != null.

Common situations: User already completed setup and clicks the setup link again; stale or reused setup links; client mistakenly routing a password-reset through the setup endpoint; duplicated signup.

Related errors


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