KeygraphHQ/shannon · error · Error
Invalid base32 character: ${char}
Error message
Invalid base32 character: ${char} What it means
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.
Source
Thrown at apps/worker/src/scripts/generate-totp.ts:38
// === Base32 Decoding ===
function base32Decode(encoded: string): Buffer {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const cleanInput = encoded.toUpperCase().replace(/[^A-Z2-7]/g, '');
if (cleanInput.length === 0) {
throw new Error('TOTP secret is empty after cleaning');
}
const output: number[] = [];
let bits = 0;
let value = 0;
for (const char of cleanInput) {
const index = alphabet.indexOf(char);
if (index === -1) {
throw new Error(`Invalid base32 character: ${char}`);
}
value = (value << 5) | index;
bits += 5;
if (bits >= 8) {
output.push((value >>> (bits - 8)) & 255);
bits -= 8;
}
}
return Buffer.from(output);
}
// === TOTP Generation (RFC 6238) ===
function generateHOTP(secret: string, counter: number, digits: number = 6): string {
const key = base32Decode(secret);View on GitHub (pinned to 1ae0a142f8)
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.
Example fix
// before - direct call with bad input
base32Decode("JBSW Y3DP!@#") // '!' survives an inconsistent cleaner
// after
base32Decode("JBSWY3DPEHPK3PXP") Defensive patterns
Strategy: validation
Validate before calling
const BASE32_RE = /^[A-Z2-7]+$/i;
function isStrictBase32(s: string): boolean {
return BASE32_RE.test(s);
} Try / catch
try {
const code = generateTOTP(secret);
} catch (e) {
if (e instanceof Error && /Invalid base32 character/.test(e.message)) {
// re-clean input and retry, or surface to caller
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- TOTP secret is empty after cleaning
- CONFIG_NOT_FOUND
- Cannot create deliverables directory at ${deliverablesDir}
- Login instructions template not found
- Failed to build login instructions: ${errMsg}
AI-assisted analysis of KeygraphHQ/shannon@1ae0a142f8 (2026-08-12).
Data as JSON: /api/errors/e0e356c9f58c3887.
Report an issue: GitHub.