{"record":{"id":"6cac15c07920cee1","repo":"Hmbown/CodeWhale","slug":"supabase-not-configured","errorCode":"supabase-not-configured","errorMessage":"supabase-not-configured","messagePattern":"supabase-not-configured","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/lib/cloud-facts.ts","lineNumber":257,"sourceCode":"function isPublishableKey(key: string): boolean {\n  if (/^sb_publishable_[A-Za-z0-9_-]+$/.test(key)) return true;\n  if (key.length > 8192) return false;\n  try {\n    const parts = key.split(\".\");\n    if (parts.length !== 3) return false;\n    const middle = parts[1].replace(/-/g, \"+\").replace(/_/g, \"/\");\n    const payload = JSON.parse(atob(middle.padEnd(Math.ceil(middle.length / 4) * 4, \"=\")));\n    return isObject(payload) && payload.role === \"anon\";\n  } catch { return false; }\n}\n\nexport async function fetchCurrentRow(channel: string, env: CloudFactsEnv, opts: ResolveOptions = {}): Promise<FactsCurrentRow | null> {\n  if (!isValidChannel(channel)) throw new Error(\"invalid-channel\");\n  const key = env.SUPABASE_PUBLISHABLE_KEY;\n  let base: URL;\n  try {\n    base = new URL(env.SUPABASE_URL ?? \"\");\n    if (base.protocol !== \"https:\" || base.username || base.password || base.search || base.hash || !key || !isPublishableKey(key)) throw new Error();\n  } catch { throw new Error(\"supabase-not-configured\"); }\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? SUPABASE_TIMEOUT_MS);\n  try {\n    const url = new URL(`${base.href.replace(/\\/+$/, \"\")}/rest/v1/facts_current`);\n    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();\n    const res = await (opts.fetchImpl ?? fetch)(url, {\n      headers: { apikey: key!, Authorization: `Bearer ${key}`, Accept: \"application/json\" },\n      signal: controller.signal,\n      redirect: \"error\",\n    });\n    if (!res.ok) { await res.body?.cancel(); throw new Error(`supabase-http-${res.status}`); }\n    const bytes = await readBoundedBody(res, MAX_ENVELOPE_BYTES);\n    const rows: unknown = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes));\n    if (!Array.isArray(rows) || rows.length > 1) throw new Error(\"supabase-bad-row\");\n    if (rows.length === 0) return null;\n    if (!isObject(rows[0]) || !isEnvelope(envelopeFromRow(rows[0] as unknown as FactsCurrentRow))) throw new Error(\"supabase-bad-row\");\n    return rows[0] as unknown as FactsCurrentRow;","sourceCodeStart":239,"sourceCodeEnd":275,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/web/lib/cloud-facts.ts#L239-L275","documentation":"fetchCurrentRow validates the Supabase REST configuration before issuing any request. If SUPABASE_URL is missing/unparseable, is not an https URL with no userinfo/query/hash, or SUPABASE_PUBLISHABLE_KEY is missing or fails isPublishableKey, the catch block rethrows as \"supabase-not-configured\". It signals the caller that cloud facts cannot be fetched because the environment is not set up for authenticated Supabase access.","triggerScenarios":"Calling fetchCurrentRow (directly or via row()) when env.SUPABASE_URL is undefined/empty or malformed, or not https, or embeds username/password/query/hash; or env.SUPABASE_PUBLISHABLE_KEY is absent or not a valid publishable (anon) key.","commonSituations":"Deploying the worker without setting SUPABASE_URL/SUPABASE_PUBLISHABLE_KEY secrets; a staging environment with only local KV; pasting a service_role key where a publishable/anon key is required; typos in env var names; a URL with a query string or trailing credentials from copy-paste.","solutions":["Set SUPABASE_URL to a clean https://<project>.supabase.co URL with no query, hash, or userinfo.","Set SUPABASE_PUBLISHABLE_KEY to the project's anon/publishable key (starts with 'sb_publishable_' or legacy 'eyJ...') via wrangler secret or environment config.","Check for typos in the env var names so values actually land in CloudFactsEnv.","If Supabase is intentionally not used in this environment, catch this error and fall back to local/curated facts instead of treating it as fatal."],"exampleFix":"// before (bad URL with query)\nSUPABASE_URL=https://xyz.supabase.co?apikey=abc\n\n// after\nSUPABASE_URL=https://xyz.supabase.co\nSUPABASE_PUBLISHABLE_KEY=sb_publishable_...","handlingStrategy":"validation","validationCode":"function supabaseConfigured(env) {\n  try {\n    const u = new URL(env.SUPABASE_URL ?? '');\n    return u.protocol === 'https:' && !u.username && !u.password && !u.search && !u.hash && !!env.SUPABASE_PUBLISHABLE_KEY;\n  } catch { return false; }\n}","typeGuard":"const isConfigured = (env) => typeof env.SUPABASE_URL === 'string' && env.SUPABASE_URL.startsWith('https://') && typeof env.SUPABASE_PUBLISHABLE_KEY === 'string' && env.SUPABASE_PUBLISHABLE_KEY.length > 0;","tryCatchPattern":null,"preventionTips":["Add SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY to a deploy-time checklist or CI smoke test.","Never paste query strings or credentials into SUPABASE_URL.","Use the anon publishable key, not service_role, in client-facing env.","Validate env vars at worker startup and fail loudly before serving traffic."],"tags":["configuration","supabase","env"],"backgroundTag":"missing-env-var","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}