{"record":{"id":"da97382889a1cf1a","repo":"Hmbown/CodeWhale","slug":"bad-channel-slug","errorCode":null,"errorMessage":"bad channel slug","messagePattern":"bad channel slug","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/scripts/facts-publish.mjs","lineNumber":536,"sourceCode":"    try { writeFileSync(fd, privateKey.export({ type: \"pkcs8\", format: \"pem\" })); }\n    finally { closeSync(fd); }\n    const raw = rawPublicKeyFromKeyObject(publicKey);\n    console.log(JSON.stringify({\n      key_id: keyId,\n      algorithm: \"ed25519\",\n      public_key_b64: raw.toString(\"base64\"),\n      public_key_bytes: [...raw],\n      private_key_file: out,\n      note: \"Private key written with mode 0600. Move it into custody (password manager); never commit it.\",\n    }, null, 2));\n    return 0;\n  }\n  if (cmd === \"sign\") {\n    refuseUnderCi();\n    const sourcePath = resolve(String(flags.source ?? resolve(REPO_ROOT, \"docs/cloud-facts/stable.json\")));\n    const source = readJson(sourcePath);\n    const channel = String(flags.channel ?? source.channel ?? \"stable\");\n    if (!CHANNEL_RE.test(channel)) throw new Error(\"bad channel slug\");\n    const factsVersion = Number(flags[\"facts-version\"] ?? source.facts_version);\n    if (!Number.isSafeInteger(factsVersion) || factsVersion <= 0) throw new Error(\"--facts-version (or source.facts_version) must be a positive integer\");\n    const keyId = String(flags[\"key-id\"] ?? \"\");\n    const privateKey = loadPrivateKeyFromEnv();\n    const publishedAt = String(flags[\"published-at\"] ?? nowIso());\n    const payload = buildPayload(source, { channel, factsVersion, publishedAt });\n    const envelope = buildEnvelope({ privateKey, keyId, payload });\n    const text = `${JSON.stringify(envelope, null, 2)}\\n`;\n    if (flags.out) {\n      writeFileSync(resolve(String(flags.out)), text);\n      console.error(`wrote ${flags.out} (channel=${channel} facts_version=${factsVersion} key_id=${keyId} sha256=${envelope.sha256})`);\n    } else process.stdout.write(text);\n    return 0;\n  }\n  if (cmd === \"verify\") {\n    const envelope = readJson(resolve(String(positional[1] ?? \"\")));\n    let pub = flags[\"public-key\"];\n    if (!pub) {","sourceCodeStart":518,"sourceCodeEnd":554,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/web/scripts/facts-publish.mjs#L518-L554","documentation":"The sign subcommand derives the channel from --channel or the source document's channel field (default \"stable\") and validates it against CHANNEL_RE. A slug containing characters outside the allowed set (uppercase, slashes, spaces, etc.) throws \"bad channel slug\".","triggerScenarios":"Running `sign --channel <bad>` with a slug failing CHANNEL_RE, or signing a source JSON whose channel field is malformed/missing-safe default is overridden with an invalid value.","commonSituations":"Using a channel name like \"Stable\" or \"beta/v2\"; a docs/cloud-facts/stable.json edited with a typo'd channel; CI automation injecting a branch name as the channel.","solutions":["Use a slug accepted by CHANNEL_RE (check its definition near the top of the script; typically lowercase [a-z0-9-])","Fix the channel field in the source JSON or pass --channel explicitly to override it","Run `verify` on a known-good envelope to see valid channel examples"],"exampleFix":"// before\nnode facts-publish.mjs sign --channel \"Beta/v2\"\n// after\nnode facts-publish.mjs sign --channel beta-v2","handlingStrategy":"validation","validationCode":"const CHANNEL_RE = /^[a-z0-9-]+$/; // mirror the script's pattern\nif (!CHANNEL_RE.test(channel)) throw new Error(`bad channel slug: ${channel}`);","typeGuard":null,"tryCatchPattern":"try {\n  await run(['sign', envelopePath, '--channel', channel]);\n} catch (e) {\n  if (e.message === 'bad channel slug') console.error('Channel must be a lowercase slug (letters, digits, hyphens)');\n  throw e;\n}","preventionTips":["Validate channel slugs in CI before signing","Keep the source JSON's channel field in the allowed format","Derive channel names from fixed config, not branch names"],"tags":["cli","validation","signing"],"backgroundTag":"invalid-identifier-format","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}