Hmbown/CodeWhale · error · Error

invalid-channel

invalid-channel

Error message

invalid-channel

What it means

fetchCurrentRow validates the channel name with isValidChannel before touching Supabase and throws "invalid-channel" for anything that fails, preventing attacker-controlled channel strings from reaching query construction.

Solutions

  1. Validate/sanitize the channel against the allowed pattern before calling fetchCurrentRow.
  2. Check where the channel value originates (route params, query string) and reject invalid values at the route with a 400.
  3. Compare against the isValidChannel pattern in web/lib/cloud-facts.ts to see exactly which characters/length are accepted.

Example fix

// before
const row = await fetchCurrentRow(params.get("channel") ?? "", env);
// after
const channel = params.get("channel") ?? "";
if (!/^[a-z0-9-]{1,64}$/.test(channel)) return new Response("invalid channel", { status: 400 });
const row = await fetchCurrentRow(channel, env);
Defensive patterns

Strategy: type-guard

Validate before calling

// Route-level guard before resolving a channel
const channel = new URL(request.url).searchParams.get("channel") ?? "";
if (!/^[a-z0-9-]{1,64}$/.test(channel)) return new Response("invalid channel", { status: 400 });

Type guard

function isSafeChannel(v: unknown): v is string {
  return typeof v === "string" && /^[a-z0-9-]{1,64}$/.test(v); // match isValidChannel's actual pattern
}

Try / catch

try {
  const row = await fetchCurrentRow(channel, env);
} catch (err) {
  if (err.message === "invalid-channel") {
    return new Response("invalid channel", { status: 400 });
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling fetchCurrentRow (via the row resolver) with a channel string failing isValidChannel — wrong characters, wrong length, wrong format, or a non-conforming value from a request parameter.

Common situations: Passing raw URL path/query segments as the channel without sanitizing; a client using an old channel naming scheme; typos or URL-decoding artifacts (e.g. %20) in the channel value.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

  if (expires !== null && now >= expires) return { ok: false, reason: "expired" };
  return { ok: true, keyId, mode: "verified" };
}

/** Only publishable keys (or legacy anon JWTs), never secret/service-role keys. */
function isPublishableKey(key: string): boolean {
  if (/^sb_publishable_[A-Za-z0-9_-]+$/.test(key)) return true;
  if (key.length > 8192) return false;
  try {
    const parts = key.split(".");
    if (parts.length !== 3) return false;
    const middle = parts[1].replace(/-/g, "+").replace(/_/g, "/");
    const payload = JSON.parse(atob(middle.padEnd(Math.ceil(middle.length / 4) * 4, "=")));
    return isObject(payload) && payload.role === "anon";
  } catch { return false; }
}

export async function fetchCurrentRow(channel: string, env: CloudFactsEnv, opts: ResolveOptions = {}): Promise<FactsCurrentRow | null> {
  if (!isValidChannel(channel)) throw new Error("invalid-channel");
  const key = env.SUPABASE_PUBLISHABLE_KEY;
  let base: URL;
  try {
    base = new URL(env.SUPABASE_URL ?? "");
    if (base.protocol !== "https:" || base.username || base.password || base.search || base.hash || !key || !isPublishableKey(key)) throw new Error();
  } catch { throw new Error("supabase-not-configured"); }
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? SUPABASE_TIMEOUT_MS);
  try {
    const url = new URL(`${base.href.replace(/\/+$/, "")}/rest/v1/facts_current`);
    url.search = new URLSearchParams({ channel: `eq.${channel}`, scope: "eq.global", select: "channel,release_id,facts_version,schema_version,envelope_version,applies_to,key_id,payload_b64,sig_b64,sigs,payload_sha256,published_at,not_after", limit: "1" }).toString();
    const res = await (opts.fetchImpl ?? fetch)(url, {
      headers: { apikey: key!, Authorization: `Bearer ${key}`, Accept: "application/json" },
      signal: controller.signal,
      redirect: "error",
    });
    if (!res.ok) { await res.body?.cancel(); throw new Error(`supabase-http-${res.status}`); }
    const bytes = await readBoundedBody(res, MAX_ENVELOPE_BYTES);

View on GitHub (pinned to 433685b202)