{"record":{"id":"d0863bfb99fc3b57","repo":"Hmbown/CodeWhale","slug":"invalid-supabase-endpoint","errorCode":null,"errorMessage":"invalid Supabase endpoint","messagePattern":"invalid Supabase endpoint","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/scripts/facts-publish.mjs","lineNumber":453,"sourceCode":"    `  on conflict (key_id) do nothing;`,\n    `insert into public.facts_release (channel_id, facts_version, schema_version, envelope_version, applies_to, key_id, payload_b64, sig_b64, sigs, payload, published_at, not_after, published_by, notes)`,\n    `  select c.id, ${envelope.facts_version}, ${envelope.schema_version}, ${envelope.envelope}, ${sqlLiteral(envelope.applies_to)}, ${sqlLiteral(envelope.key_id)},`,\n    `         ${sqlLiteral(envelope.payload_b64)}, ${sqlLiteral(envelope.sig_b64)}, ${sqlLiteral(JSON.stringify(envelope.sigs ?? []))}::jsonb,`,\n    `         ${sqlLiteral(payloadJson)}::jsonb, ${sqlLiteral(envelope.published_at)}::timestamptz, ${sqlLiteral(check.payload.not_after ?? null)}::timestamptz,`,\n    `         ${sqlLiteral(publishedBy)}, ${sqlLiteral(notes)}`,\n    `    from public.facts_channel c where c.scope = 'global' and c.slug = ${sqlLiteral(envelope.channel)};`,\n    \"commit;\",\n    \"\",\n  ].join(\"\\n\");\n}\n\nasync function postgrest(path, { method = \"GET\", body, prefer } = {}) {\n  refuseUnderCi();\n  const url = process.env.SUPABASE_URL;\n  const key = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY;\n  if (!url || !key) throw new Error(\"SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required\");\n  const endpoint = new URL(url);\n  if (endpoint.protocol !== \"https:\" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) throw new Error(\"invalid Supabase endpoint\");\n  const res = await fetch(`${url.replace(/\\/$/, \"\")}/rest/v1/${path}`, {\n    method,\n    signal: AbortSignal.timeout(30_000),\n    redirect: \"error\",\n    headers: {\n      apikey: key,\n      Authorization: `Bearer ${key}`,\n      \"Content-Type\": \"application/json\",\n      ...(prefer ? { Prefer: prefer } : {}),\n    },\n    body: body === undefined ? undefined : JSON.stringify(body),\n  });\n  if (!res.ok) { await res.body?.cancel(); throw new Error(`PostgREST request failed (HTTP ${res.status})`); }\n  const text = await readBoundedResponse(res);\n  return text ? JSON.parse(text) : null;\n}\n\nexport async function readBoundedResponse(response, maxBytes = MAX_ENVELOPE_BYTES) {","sourceCodeStart":435,"sourceCodeEnd":471,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/web/scripts/facts-publish.mjs#L435-L471","documentation":"postgrest parses SUPABASE_URL with new URL() and enforces a clean https origin: protocol must be https and username, password, search (query), and hash must all be empty. Anything else is rejected to stop the service-role key from being sent to a malformed or attacker-influenced endpoint.","triggerScenarios":"Calling postgrest when SUPABASE_URL is http://, contains a query string or fragment (e.g. a pasted URL with ?key=... or #/), embeds userinfo (user:pass@host), is not a valid URL at all (new URL throws separately), or includes a trailing path with parameters.","commonSituations":"A URL was copied from the Supabase dashboard including query/hash decorations; a staging proxy used http://; the env var accidentally holds the REST base with ?apikey=... appended; a typo like https:/host (single slash) fails URL parsing.","solutions":["Set SUPABASE_URL to the bare https project origin, e.g. https://<projectref>.supabase.co with no query, fragment, credentials, or trailing path","Strip any ?... or #... that was pasted along with the URL and re-export the variable","If you need a non-standard endpoint (self-hosted proxy), it must still be https with a clean origin; fix the proxy URL rather than the env value"],"exampleFix":"// before\nSUPABASE_URL=https://xyz.supabase.co/rest/v1?apikey=abc\n// after\nSUPABASE_URL=https://xyz.supabase.co","handlingStrategy":"validation","validationCode":"function assertCleanSupabaseUrl(raw) {\n  const u = new URL(raw); // throws separately on unparseable URLs\n  const bad = u.protocol !== \"https:\" || u.username || u.password || u.search || u.hash;\n  if (bad) throw new Error(`SUPABASE_URL must be a bare https origin, got: ${raw}`);\n  return u.origin;\n}\nassertCleanSupabaseUrl(process.env.SUPABASE_URL);","typeGuard":"function isCleanHttpsOrigin(raw) {\n  try {\n    const u = new URL(raw);\n    return u.protocol === \"https:\" && !u.username && !u.password && !u.search && !u.hash;\n  } catch {\n    return false;\n  }\n}","tryCatchPattern":"try {\n  await publishFacts(envelope);\n} catch (err) {\n  if (err.message === \"invalid Supabase endpoint\") {\n    console.error(\"SUPABASE_URL must be a clean https origin (no query, fragment, credentials, or http) — fix the env value\");\n    process.exit(1);\n  }\n  throw err;\n}","preventionTips":["Store the bare project origin (https://<ref>.supabase.co) in env; never paste URLs with ?apikey=... or #...","Add a startup check that validates SUPABASE_URL with the same rules before any network call","For self-hosted endpoints, keep https and a clean origin; put paths/params in code, not the env value"],"tags":["url-validation","supabase","security"],"backgroundTag":"invalid-url-format","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"}