{"record":{"id":"e0e356c9f58c3887","repo":"KeygraphHQ/shannon","slug":"invalid-base32-character-char","errorCode":null,"errorMessage":"Invalid base32 character: ${char}","messagePattern":"Invalid base32 character: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/worker/src/scripts/generate-totp.ts","lineNumber":38,"sourceCode":"\n// === Base32 Decoding ===\n\nfunction base32Decode(encoded: string): Buffer {\n  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\n  const cleanInput = encoded.toUpperCase().replace(/[^A-Z2-7]/g, '');\n\n  if (cleanInput.length === 0) {\n    throw new Error('TOTP secret is empty after cleaning');\n  }\n\n  const output: number[] = [];\n  let bits = 0;\n  let value = 0;\n\n  for (const char of cleanInput) {\n    const index = alphabet.indexOf(char);\n    if (index === -1) {\n      throw new Error(`Invalid base32 character: ${char}`);\n    }\n\n    value = (value << 5) | index;\n    bits += 5;\n\n    if (bits >= 8) {\n      output.push((value >>> (bits - 8)) & 255);\n      bits -= 8;\n    }\n  }\n\n  return Buffer.from(output);\n}\n\n// === TOTP Generation (RFC 6238) ===\n\nfunction generateHOTP(secret: string, counter: number, digits: number = 6): string {\n  const key = base32Decode(secret);","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/KeygraphHQ/shannon/blob/1ae0a142f8525410a688f0309fd003cc5b1d92de/apps/worker/src/scripts/generate-totp.ts#L20-L56","documentation":"Thrown by base32Decode in generate-totp when a post-cleaning character is somehow not in the base32 alphabet. Because cleanInput already strips non-[A-Z2-7] chars via regex, this branch is normally unreachable through the CLI; it exists as a defensive guard for direct callers and future alphabet changes. Surfaced to CLI users as a JSON error with retryable:false and exit code 1.","triggerScenarios":"Effectively unreachable via the generate-totp CLI: cleanInput only contains A-Z and 2-7 chars by construction, all of which are in alphabet. Could fire only if base32Decode is called directly with a pre-cleaned string containing other characters, or if the alphabet constant is changed to drop a character.","commonSituations":"Calling base32Decode in tests with already-cleaned input that violates the alphabet; modifying the alphabet or the cleaning regex inconsistently; future regressions in the cleaning step.","solutions":["If scripting, run /^[A-Z2-7]+$/i on the raw secret first.","If forking, keep the alphabet and the cleaning regex in sync.","Use the CLI as-is rather than calling base32Decode directly."],"exampleFix":"// before - direct call with bad input\nbase32Decode(\"JBSW Y3DP!@#\")  // '!' survives an inconsistent cleaner\n// after\nbase32Decode(\"JBSWY3DPEHPK3PXP\")","handlingStrategy":"validation","validationCode":"const BASE32_RE = /^[A-Z2-7]+$/i;\nfunction isStrictBase32(s: string): boolean {\n  return BASE32_RE.test(s);\n}","typeGuard":null,"tryCatchPattern":"try {\n  const code = generateTOTP(secret);\n} catch (e) {\n  if (e instanceof Error && /Invalid base32 character/.test(e.message)) {\n    // re-clean input and retry, or surface to caller\n  } else throw e;\n}","preventionTips":["Validate the raw secret against /^[A-Z2-7]+$/i before calling base32Decode.","If forking the script, keep the alphabet constant and the cleaning regex consistent.","Use the bundled CLI rather than calling base32Decode directly."],"tags":["totp","cli","base32","authentication"],"backgroundTag":null,"analyzedSha":"1ae0a142f8525410a688f0309fd003cc5b1d92de","analyzedAt":"2026-08-12T17:40:03.583Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}