{"record":{"id":"ea51e901bc64d644","repo":"Hmbown/CodeWhale","slug":"unparsed-typescript-trusted-keys-entry","errorCode":null,"errorMessage":"unparsed TypeScript TRUSTED_KEYS entry","messagePattern":"unparsed TypeScript TRUSTED_KEYS entry","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/scripts/facts-publish.mjs","lineNumber":394,"sourceCode":"    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\");\n  if (!key) throw new Error(\"primary signing key is not pinned and active; refusing publication\");\n  const check = verifyEnvelope(envelope, key.publicKey);\n  if (!check.ok) throw new Error(`envelope does not verify: ${check.errors.join(\"; \")}`);\n  if (!Number.isFinite(now) || utcTime(check.payload.published_at) > now + 300_000 ||\n      (check.payload.not_after != null && utcTime(check.payload.not_after) <= now)) throw new Error(\"publication timestamp is future or expired\");\n  return { key, check };\n}\n","sourceCodeStart":376,"sourceCodeEnd":412,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/web/scripts/facts-publish.mjs#L376-L412","documentation":"After parseTsKeys extracts the TRUSTED_KEYS table body, it peels out every entry matching the strict `{ keyId: \"...\", publicKey: \"...\", status: \"...\" }` shape and requires that nothing but whitespace and commas remains. Any leftover text means an entry did not match the strict shape, so the gate refuses rather than publishing with a partially parsed key list.","triggerScenarios":"Calling parseTsKeys when a TRUSTED_KEYS entry uses different field order, single quotes, missing/extra fields, spread syntax, computed values, helper constructors, or trailing inline comments that survive the line-comment strip inside the array body.","commonSituations":"A contributor added a key entry with fields in a different order or via an object variable; a key was added with a trailing comma-comment on the same line as the closing brace; the status field uses a type or template literal instead of a plain double-quoted string.","solutions":["Reformat the offending entry in keys.ts to exactly `{ keyId: \"...\", publicKey: \"...\", status: \"...\" }` with double-quoted literal strings in that field order","Move any inline comments to their own line (line comments on separate lines are stripped) or remove them from inside the array","Run the publish script's parse step locally on the edited keys.ts to confirm the error is gone before publishing"],"exampleFix":"// before\n{ status: \"active\", keyId: \"k1\", publicKey: \"abc\" } // rotated\n// after\n{ keyId: \"k1\", publicKey: \"abc\", status: \"active\" }","handlingStrategy":"validation","validationCode":"const body = text.match(/TRUSTED_KEYS[^=]*=\\s*\\[([\\s\\S]*?)\\];/)?.[1] ?? \"\";\nconst unparsed = body\n  .replace(/\\{\\s*keyId:\\s*\"[^\"]+\",\\s*publicKey:\\s*\"[^\"]+\",\\s*status:\\s*\"[^\"]+\"\\s*,?\\s*\\}/g, \"\")\n  .replace(/[\\s,]/g, \"\");\nif (unparsed) throw new Error(\"keys.ts contains entries outside the strict entry shape: \" + unparsed.slice(0, 80));","typeGuard":null,"tryCatchPattern":"try {\n  const keys = parseTsKeys(text);\n} catch (err) {\n  if (err.message === \"unparsed TypeScript TRUSTED_KEYS entry\") {\n    console.error(\"An entry in TRUSTED_KEYS does not match { keyId, publicKey, status } with double-quoted literals in that order\");\n    process.exit(1);\n  }\n  throw err;\n}","preventionTips":["Always add new keys in the exact field order keyId, publicKey, status with double-quoted string literals","Put comments on their own lines, never inline inside an entry object","Lint keys.ts with a small check script so malformed entries are caught before publishing"],"tags":["typescript","publishing-gate","schema-validation"],"backgroundTag":"schema-validation-failed","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"}