{"record":{"id":"9372983b7cd0fa51","repo":"Hmbown/CodeWhale","slug":"cannot-parse-exactly-one-typescript-trusted-keys-table","errorCode":null,"errorMessage":"cannot parse exactly one TypeScript TRUSTED_KEYS table","messagePattern":"cannot parse exactly one TypeScript TRUSTED_KEYS table","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/scripts/facts-publish.mjs","lineNumber":386,"sourceCode":"  const key = createPrivateKey({ key: pem, format: \"pem\" });\n  if (key.asymmetricKeyType !== \"ed25519\") throw new Error(\"signing key must be Ed25519\");\n  return key;\n}\n\nexport function validateTrustedKeys(keys) {\n  const seen = new Set();\n  for (const key of keys) {\n    if (!KEY_ID_RE.test(key.keyId) || seen.has(key.keyId) || ![\"active\", \"retired\"].includes(key.status) || strictBase64(key.publicKey, 32).length !== 32) throw new Error(\"invalid or duplicated pinned key\");\n    seen.add(key.keyId);\n  }\n  return keys;\n}\n\n/** Deliberately narrow syntax: a changed/unparseable table must fail the gate. */\nexport function parseTsKeys(text) {\n  const source = text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\").replace(/^\\s*\\/\\/.*$/gm, \"\");\n  const tables = [...source.matchAll(/^\\s*export\\s+const\\s+TRUSTED_KEYS\\s*:\\s*readonly\\s+TrustedKey\\[\\]\\s*=\\s*\\[([\\s\\S]*?)\\]\\s*;/gm)];\n  if (tables.length !== 1) throw new Error(\"cannot parse exactly one TypeScript TRUSTED_KEYS table\");\n  const table = tables[0];\n  const body = table[1].replace(/^\\s*\\/\\/.*$/gm, \"\");\n  const keys = [];\n  const remainder = body.replace(/\\{\\s*keyId:\\s*\"([^\"]+)\",\\s*publicKey:\\s*\"([^\"]+)\",\\s*status:\\s*\"([^\"]+)\"\\s*,?\\s*\\}/g, (_, keyId, publicKey, status) => {\n    keys.push({ keyId, publicKey, status });\n    return \"\";\n  });\n  if (remainder.replace(/[\\s,]/g, \"\")) throw new Error(\"unparsed TypeScript TRUSTED_KEYS entry\");\n  return validateTrustedKeys(keys);\n}\n\nfunction loadTrustedKeysFromRepo() {\n  const keys = parseTsKeys(readBoundedFile(resolve(WEB_ROOT, \"lib/cloud-facts/keys.ts\"), 64 * 1024).toString(\"utf8\"));\n  return new Map(keys.map((key) => [key.keyId, key]));\n}\n\nexport function activePublishingKey(envelope, keys, now = Date.now()) {\n  const key = validateTrustedKeys(keys).find((key) => key.keyId === envelope.key_id && key.status === \"active\");","sourceCodeStart":368,"sourceCodeEnd":404,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/web/scripts/facts-publish.mjs#L368-L404","documentation":"parseTsKeys extracts the pinned TRUSTED_KEYS table from web/lib/cloud-facts/keys.ts using a deliberately narrow regex over the source. It requires exactly one match for the `export const TRUSTED_KEYS: readonly TrustedKey[] = [...]` declaration; if zero or multiple tables are found the gate fails closed so a drifted or renamed key table cannot be silently ignored.","triggerScenarios":"Calling parseTsKeys on file text where (a) the TRUSTED_KEYS declaration is missing/renamed, (b) its type annotation no longer reads exactly `: readonly TrustedKey[]`, (c) it was changed to a non-array or moved into a comment (comments are stripped first), or (d) the file accidentally declares the table twice.","commonSituations":"A refactor renamed TRUSTED_KEYS or changed its type annotation; a merge duplicated the const; someone reformatted the declaration with a line break inside the type annotation; the keys file was replaced wholesale with a JSON file or different structure.","solutions":["Open web/lib/cloud-facts/keys.ts and ensure there is exactly one `export const TRUSTED_KEYS: readonly TrustedKey[] = [ ... ];` declaration, matching that exact type annotation","Remove any duplicate or commented-out second declaration of TRUSTED_KEYS from the file","If the key-table format intentionally changed, update parseTsKeys and its tests together rather than loosening the regex silently"],"exampleFix":"// before (keys.ts)\nexport const TRUSTED_KEYS: TrustedKey[] = [ ... ];\n// after\nexport const TRUSTED_KEYS: readonly TrustedKey[] = [ ... ];","handlingStrategy":"validation","validationCode":"import { readFileSync } from \"node:fs\";\nconst text = readFileSync(\"web/lib/cloud-facts/keys.ts\", \"utf8\");\nconst matches = [...text.matchAll(/export\\s+const\\s+TRUSTED_KEYS\\s*:/g)];\nif (matches.length !== 1) throw new Error(`expected exactly one TRUSTED_KEYS declaration, found ${matches.length}`);","typeGuard":null,"tryCatchPattern":"try {\n  const keys = parseTsKeys(text);\n} catch (err) {\n  if (err.message.includes(\"exactly one TypeScript TRUSTED_KEYS\")) {\n    console.error(\"keys.ts table declaration missing, duplicated, or reformatted — fix web/lib/cloud-facts/keys.ts\");\n    process.exit(1);\n  }\n  throw err;\n}","preventionTips":["Never rename TRUSTED_KEYS or change its `readonly TrustedKey[]` type annotation without updating the publish script","Keep exactly one declaration; delete commented-out copies of the table","Run the publish script's parse step in CI (with publishing disabled) to catch drift early"],"tags":["typescript","publishing-gate","key-pinning"],"backgroundTag":"invalid-argument-format","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}