Hmbown/CodeWhale · error · Error

invalid-base64

invalid-base64

Error message

invalid-base64

What it means

b64ToBytes validates base64 input before decoding: the value must be a non-empty string, fit the maxBytes budget, have length divisible by 4, match the standard base64 alphabet with at most two padding '='s. It throws "invalid-base64" if any structural check fails, before atob allocates.

Solutions

  1. Re-encode the value with standard base64 (btoa/Buffer.toString('base64')), which pads to a multiple of 4.
  2. Convert URL-safe base64 to standard: replace '-' with '+' and '_' with '/', then pad with '=' to a multiple of 4.
  3. Trim surrounding whitespace and ensure no internal line breaks before passing the value.
  4. Check the decoded byte size is within maxBytes for the field (keys, signatures, envelopes have separate caps).

Example fix

// before
const token = btoaUrlSafe(raw).replace(/=+$/, "");
// after
const token = btoa(raw); // standard alphabet, padded, length % 4 === 0
Defensive patterns

Strategy: validation

Validate before calling

function isStandardBase64(v: unknown, maxLen: number): boolean {
  return typeof v === "string" && v.length > 0 && v.length <= maxLen &&
    v.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(v);
}

Type guard

function isBase64String(v: unknown): v is string {
  return typeof v === "string" && v.length > 0 && v.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(v);
}

Try / catch

try {
  const bytes = b64ToBytes(value, maxBytes);
} catch (err) {
  if (err.message === "invalid-base64") {
    // Re-encode or reject the input; do not retry the same value.
    value = btoa(String(value).replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, ""));
  }
}

Prevention

When it happens

Trigger: Passing a non-string, empty string, URL-safe base64 (using - and _), base64 without padding, whitespace/newlines inside the value, or a string longer than 4*ceil(maxBytes/3) to any consumer of b64ToBytes (hasActiveKeys, validSignature, verifyEnvelope, publicKey).

Common situations: Generating tokens with base64url (JWT-style) encoders; hand-trimming or concatenating base64 chunks; copying values with trailing whitespace or quotes; oversized values from upstream.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/44e1d9c75bfa3dfb. Report an issue: GitHub.

Appendix: source

Thrown at web/lib/cloud-facts.ts:95

  fetchImpl?: typeof fetch;
  keys?: readonly TrustedKey[];
  timeoutMs?: number;
  now?: () => number;
}

export function isValidChannel(slug: string): boolean {
  return CHANNEL_RE.test(slug);
}

function isObject(value: unknown): value is Record<string, unknown> {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

/** Reject noncanonical/oversized encodings before either decoder allocates. */
function b64ToBytes(value: unknown, maxBytes: number): Uint8Array {
  if (typeof value !== "string" || !value.length || value.length > 4 * Math.ceil(maxBytes / 3) ||
      (value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value))) {
    throw new Error("invalid-base64");
  }
  const bin = atob(value);
  if (bin.length > maxBytes || btoa(bin) !== value) throw new Error("invalid-base64");
  return Uint8Array.from(bin, (char) => char.charCodeAt(0));
}

export async function sha256Hex(bytes: Uint8Array): Promise<string> {
  const digest = await crypto.subtle.digest("SHA-256", bytes as BufferSource);
  return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
}

export function signingMessage(keyId: string, payload: Uint8Array): Uint8Array {
  const prefix = new TextEncoder().encode(`${DOMAIN}${keyId}\0`);
  const out = new Uint8Array(prefix.length + payload.length);
  out.set(prefix);
  out.set(payload, prefix.length);
  return out;
}

View on GitHub (pinned to 433685b202)