{"record":{"id":"d76721727023c844","repo":"affaan-m/ECC","slug":"invalid-discord-webhook-url","errorCode":null,"errorMessage":"invalid Discord webhook URL","messagePattern":"invalid Discord webhook URL","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/discord/announcement-core.mjs","lineNumber":58,"sourceCode":"      description,\n      url: String(url || ''),\n      footer: { text: footer },\n    }],\n  };\n}\n\nexport function findDiscordReceipt(messages, key) {\n  const discussionId = String(key).split(':').at(-1);\n  return messages.find(message => message.embeds?.some(embed => embed.footer?.text === `ecc:${discussionId}`)) || null;\n}\n\nexport function normalizeDiscordWebhookUrl(value) {\n  const raw = String(value || '').trim();\n  let parsed;\n  try {\n    parsed = new URL(raw);\n  } catch {\n    throw new Error('invalid Discord webhook URL');\n  }\n  if (parsed.protocol !== 'https:' || parsed.hostname !== 'discord.com' || parsed.port || parsed.username || parsed.password || parsed.search || parsed.hash) {\n    throw new Error('invalid Discord webhook URL');\n  }\n  if (!/^\\/api\\/webhooks\\/\\d{10,25}\\/[A-Za-z0-9._-]{20,}$/.test(parsed.pathname)) {\n    throw new Error('invalid Discord webhook URL');\n  }\n  parsed.search = '?wait=true';\n  return parsed.toString();\n}\n\nexport function discussionReceiptMarker(key) {\n  return `<!-- ecc-discord-receipt:${createHash('sha256').update(String(key)).digest('hex').slice(0, 32)} -->`;\n}\n\nexport function findDiscussionReceipt(comments, marker) {\n  const trusted = comments.filter(comment => (\n    ['github-actions', 'github-actions[bot]'].includes(comment?.author?.login)","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/discord/announcement-core.mjs#L40-L76","documentation":"normalizeDiscordWebhookUrl parses the webhook URL and throws 'invalid Discord webhook URL' when new URL() fails — i.e. the value is not an absolute, well-formed URL at all. This is the first of three validation stages: URL parseability, then host/protocol constraints, then path shape.","triggerScenarios":"main() passes an empty/undefined webhook value, a bare token without scheme ('discord.com/api/webhooks/...'), a URL with a typo in the scheme ('https:/discord.com/...'), or a non-URL string from an env var or secret store.","commonSituations":"DISCORD_WEBHOOK_URL env var unset or quoted incorrectly in workflow YAML; secret pasted without the leading https://; multiline paste introducing whitespace/newlines into the string.","solutions":["Set DISCORD_WEBHOOK_URL to the full webhook URL copied from Discord (Server Settings -> Integrations -> Webhooks -> Copy URL), including 'https://'.","Trim whitespace/newlines: normalizeDiscordWebhookUrl(value.trim().replace(/\\s+/g, '')).","Verify the secret in the CI workflow was saved with the complete URL and not truncated by the secret editor.","Check locally: node -e \"new URL(process.env.DISCORD_WEBHOOK_URL)\" should not throw."],"exampleFix":"// before\nconst url = normalizeDiscordWebhookUrl('discord.com/api/webhooks/123/abc');\n// after\nconst url = normalizeDiscordWebhookUrl('https://discord.com/api/webhooks/123/abc');","handlingStrategy":"validation","validationCode":"function looksLikeWebhookUrl(v) {\n  if (typeof v !== 'string') return false;\n  try { return new URL(v.trim().replace(/\\s+/g, '')).protocol === 'https:'; } catch { return false; }\n}\nif (!looksLikeWebhookUrl(process.env.DISCORD_WEBHOOK_URL)) throw new Error('DISCORD_WEBHOOK_URL must be an absolute https URL');","typeGuard":"const isParseableUrl = (v) => { try { new URL(typeof v === 'string' ? v.trim() : ''); return true; } catch { return false; } };","tryCatchPattern":"try {\n  const url = normalizeDiscordWebhookUrl(env.DISCORD_WEBHOOK_URL);\n} catch (err) {\n  if (err.message === 'invalid Discord webhook URL') {\n    console.error('DISCORD_WEBHOOK_URL must be a full https://discord.com/api/webhooks/... URL');\n    process.exit(1);\n  }\n  throw err;\n}","preventionTips":["Copy the webhook URL verbatim from Discord's Integrations UI, scheme included.","Trim whitespace and strip quotes/newlines when the value comes from YAML secrets.","Check the secret is not truncated in the CI secret editor.","Run a node one-liner new URL(...) check locally before committing config."],"tags":["validation","discord","url"],"backgroundTag":"invalid-url-format","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}