actualbudget/actual · error

Invalid user IDs

Error message

Invalid user IDs

What it means

transferAllFilesFromUser(ownerId, oldUserId) validates both arguments up front and throws 'Invalid user IDs' if either ownerId or oldUserId is falsy (empty string, null, undefined). It is called from loginWithOpenIdFinalize to move all files off a duplicate account onto the logging-in user's account.

Source

Thrown at packages/sync-server/src/services/user-service.ts:111

    if (changes > 0) {
      getAccountDb().mutate('DELETE FROM sessions WHERE user_id = ?', [userId]);
    }
    return changes;
  });
}
export function deleteUserAccess(userId) {
  try {
    return getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [
      userId,
    ]).changes;
  } catch (error) {
    throw new Error(`Failed to delete user access: ${error.message}`);
  }
}

export function transferAllFilesFromUser(ownerId, oldUserId) {
  if (!ownerId || !oldUserId) {
    throw new Error('Invalid user IDs');
  }
  try {
    getAccountDb().transaction(() => {
      const ownerExists = getUserById(ownerId);
      if (!ownerExists) {
        throw new Error('New owner not found');
      }
      getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [
        ownerId,
        oldUserId,
      ]);
    });
  } catch (error) {
    throw new Error(`Failed to transfer files: ${error.message}`);
  }
}

export function updateFileOwner(ownerId, fileId) {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Check that the OpenID provider returns a stable unique identifier (sub claim) mapped to Actual's user id.
  2. Log both IDs before the transfer and confirm neither is empty in loginWithOpenIdFinalize.
  3. Resolve the duplicate-account situation manually via the admin users API if the transfer keeps failing.
  4. Fix user creation logic so accounts are never created with empty user ids.

Example fix

// before
await transferAllFilesFromUser(newUser.user_id, dupUser.user_id); // dupUser may be null -> 'Invalid user IDs'
// after
if (newUser?.user_id && dupUser?.user_id) {
  await transferAllFilesFromUser(newUser.user_id, dupUser.user_id);
}
Defensive patterns

Strategy: validation

Validate before calling

if (!ownerId || !oldUserId) {
  throw new Error(`transferAllFilesFromUser requires both ids; got owner=${ownerId}, old=${oldUserId}`);
}
// safe to call:
await transferAllFilesFromUser(ownerId, oldUserId);

Type guard

function isTransferableUser(u: { user_id?: string } | null | undefined): u is { user_id: string } {
  return u !== null && u !== undefined && typeof u.user_id === 'string' && u.user_id.length > 0;
}

Try / catch

try {
  await transferAllFilesFromUser(ownerId, oldUserId);
} catch (err) {
  if (err instanceof Error && err.message === 'Invalid user IDs') {
    logger.error('OpenID login finalize produced an empty user id; check provider sub claim mapping');
    throw new Error('Login failed: account identifiers missing');
  }
  throw err;
}

Prevention

When it happens

Trigger: OpenID login finalization where the resulting user id or the duplicate user's id is missing/empty — e.g. the OIDC provider returned no usable subject/user id, or the duplicate account lookup returned nothing but the code still attempted the transfer.

Common situations: Misconfigured OpenID providers not returning a stable unique claim; users logging in with different email casing creating empty/partial account records; custom integrations calling transferAllFilesFromUser with unset variables.

Understand the failure class

Background: Missing required parameter errors: what 'X is required' and 'the required X param is missing' mean, and how to fix them — this error's family across 27 libraries.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/355f53adde6540a1. Report an issue: GitHub.