{"record":{"id":"052920b6303d9374","repo":"KeygraphHQ/shannon","slug":"totp-secret-is-empty-after-cleaning","errorCode":null,"errorMessage":"TOTP secret is empty after cleaning","messagePattern":"TOTP secret is empty after cleaning","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/worker/src/scripts/generate-totp.ts","lineNumber":28,"sourceCode":" * generate-totp CLI\n *\n * Generates a TOTP code for the target's MFA.\n * Based on RFC 6238 (TOTP) and RFC 4226 (HOTP).\n *\n * Usage:\n *   generate-totp --secret JBSWY3DPEHPK3PXP\n */\n\nimport { createHmac } from 'node:crypto';\n\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;","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/KeygraphHQ/shannon/blob/1ae0a142f8525410a688f0309fd003cc5b1d92de/apps/worker/src/scripts/generate-totp.ts#L10-L46","documentation":"Thrown by base32Decode in generate-totp when the input, after uppercasing and stripping non-[A-Z2-7] characters, is empty - meaning the supplied --secret contained no valid base32 characters at all. In the CLI path this is effectively shadowed by the upstream base32-regex check (which emits 'Secret must be base32-encoded' first), so it mainly surfaces when base32Decode is called directly or in tests. The CLI wraps it as a JSON error with retryable:false and exit code 1.","triggerScenarios":"generate-totp --secret is passed a string that, after removing non-base32 chars, leaves zero characters - e.g. a string of only spaces, padding, or non-letter symbols. In normal CLI flow the regex gate at line 135-145 catches this first with a different message; this throw fires only if that gate is bypassed.","commonSituations":"Calling base32Decode directly in a test with '' or '===='; an upstream code change that removes the regex pre-check; passing a secret whose only characters are base32 padding ('=') and whitespace.","solutions":["Supply a non-empty base32 secret (A-Z, 2-7) via --secret.","If scripting around generate-totp, validate with /^[A-Z2-7]+$/i before invoking.","Restore the upstream base32 regex check if you have forked the CLI."],"exampleFix":"# before\ngenerate-totp --secret \"====\"\n# after\ngenerate-totp --secret \"JBSWY3DPEHPK3PXP\"","handlingStrategy":"validation","validationCode":"function hasBase32Content(s: string): boolean {\n  return /[A-Z2-7]/i.test(s);\n}","typeGuard":null,"tryCatchPattern":"try {\n  const code = generateTOTP(secret);\n} catch (e) {\n  if (e instanceof Error && /empty after cleaning/.test(e.message)) {\n    // prompt for a valid --secret\n  } else throw e;\n}","preventionTips":["Validate the secret with /^[A-Z2-7]+$/i before calling generateTOTP/base32Decode.","Strip padding and whitespace upstream, then assert length > 0.","Treat empty/whitespace-only --secret as a missing-argument error in any wrapper."],"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"}