Hmbown/CodeWhale · error · Error

--key-id must match cwf-[a-z0-9-]

Error message

--key-id must match cwf-[a-z0-9-]{1,32}

What it means

The keygen subcommand validates the --key-id flag against KEY_ID_RE (cwf-[a-z0-9-]{1,32}) and throws this if it does not match. Key IDs are used as database identifiers in the pinned trust table, so the format is enforced up front.

Solutions

  1. Pass an id matching cwf- followed by 1-32 lowercase letters, digits, or hyphens, e.g. --key-id cwf-prod-2026-01
  2. Lowercase and hyphenate the intended name before generating the key
  3. Re-run with quotes removed and no whitespace in the flag value

Example fix

// before
node facts-publish.mjs keygen --key-id "Codewhale_Prod"
// after
node facts-publish.mjs keygen --key-id cwf-codewhale-prod
Defensive patterns

Strategy: validation

Validate before calling

const KEY_ID_RE = /^cwf-[a-z0-9-]{1,32}$/;
if (!KEY_ID_RE.test(keyId)) throw new Error(`key id must match cwf-[a-z0-9-]{1,32}, got: ${keyId}`);

Try / catch

try {
  await run(['keygen', '--key-id', keyId, '--out', outPath]);
} catch (e) {
  if (e.message.includes('--key-id must match')) console.error('Use lowercase letters, digits, hyphens only, 1-32 chars after cwf-');
  throw e;
}

Prevention

When it happens

Trigger: Running `facts-publish.mjs keygen --key-id <bad>` where the id is missing, uppercase, too long (>32 chars after the prefix), missing the cwf- prefix, or contains characters outside [a-z0-9-] (dots, underscores, spaces).

Common situations: Typing a human-readable id like "Codewhale_Prod_2026" or "stable.v2"; omitting --key-id entirely so it becomes an empty string; copying a key id with surrounding whitespace or quotes.

Understand the failure class

Background: "Unknown argument", "Invalid value", and "must be one of": invalid CLI argument errors explained — this error's family across 35 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/8d27f7823c49fae5. Report an issue: GitHub.

Appendix: source

Thrown at web/scripts/facts-publish.mjs:511

function readJson(path) {
  return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readBoundedFile(path)));
}

function nowIso() {
  return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
}

async function main(argv) {
  const { positional, flags } = parseArgs(argv);
  const cmd = positional[0];
  if (!cmd || flags.help) {
    console.log(readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(1, 26).join("\n"));
    return 0;
  }
  if (cmd === "keygen") {
    const keyId = String(flags["key-id"] ?? "");
    if (!KEY_ID_RE.test(keyId)) throw new Error("--key-id must match cwf-[a-z0-9-]{1,32}");
    const out = flags.out ? resolve(String(flags.out)) : null;
    if (!out) throw new Error("--out <path> is required (write the private key OUTSIDE any repository)");
    refuseUnderCi();
    const { privateKey, publicKey } = generateKeyPairSync("ed25519");
    mkdirSync(dirname(out), { recursive: true, mode: 0o700 });
    const fd = openSync(out, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
    try { writeFileSync(fd, privateKey.export({ type: "pkcs8", format: "pem" })); }
    finally { closeSync(fd); }
    const raw = rawPublicKeyFromKeyObject(publicKey);
    console.log(JSON.stringify({
      key_id: keyId,
      algorithm: "ed25519",
      public_key_b64: raw.toString("base64"),
      public_key_bytes: [...raw],
      private_key_file: out,
      note: "Private key written with mode 0600. Move it into custody (password manager); never commit it.",
    }, null, 2));
    return 0;

View on GitHub (pinned to 433685b202)