← back to Govarbitrage

apps/mobile/lib/auth-headers.ts

35 lines

/**
 * Pure auth-header policy — zero native dependencies so it is unit-testable.
 *
 * The GovArbitrage app never sniffs the client type and never carries a secret
 * machine-token path. A request carries an Authorization header ONLY when the
 * user has taken an explicit, visible action, with this precedence:
 *   1. Signed in with Apple  → Bearer <app session JWT>
 *   2. Self-hosting creds set → Basic base64(user:pass)
 *   3. otherwise              → no header (anonymous public read)
 */

export function buildBasicAuthHeader(username: string, password: string): string {
  // UTF-8-safe base64. Plain btoa() throws InvalidCharacterError on any code
  // point > 255 (emoji, CJK, accented letters, smart quotes); since this runs
  // BEFORE apiFetch's try/timeout block, that would propagate a raw Error out of
  // every request for a self-hosting user with a non-Latin1 password. Encode to
  // UTF-8 bytes first so btoa() only ever sees a Latin1 string.
  const bytes = new TextEncoder().encode(`${username}:${password}`);
  let binary = "";
  for (const b of bytes) binary += String.fromCharCode(b);
  return `Basic ${btoa(binary)}`;
}

export function authHeadersFor(input: {
  appleToken?: string | null;
  username?: string;
  password?: string;
}): Record<string, string> {
  if (input.appleToken) return { Authorization: `Bearer ${input.appleToken}` };
  if (input.password) {
    return { Authorization: buildBasicAuthHeader(input.username ?? "", input.password) };
  }
  return {};
}