{"record":{"id":"e7da7cd4c33c7f80","repo":"Hmbown/CodeWhale","slug":"invalid-draft-key","errorCode":null,"errorMessage":"invalid draft key","messagePattern":"invalid draft key","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/lib/community-agent.ts","lineNumber":317,"sourceCode":"  } catch {\n    return null;\n  }\n}\n\nexport async function listDrafts(kv: KVNamespace | undefined, prefix = \"draft:\"): Promise<AgentDraft[]> {\n  if (!kv) return [];\n  const listed = await kv.list({ prefix, limit: 100 });\n  const drafts: AgentDraft[] = [];\n  for (const k of listed.keys) {\n    const draft = await getDraft(kv, k.name);\n    if (draft) drafts.push(draft);\n  }\n  return drafts;\n}\n\nexport async function deleteDraft(kv: KVNamespace | undefined, key: string): Promise<void> {\n  if (!kv) return;\n  if (!parseDraftKey(key)) throw new Error(\"invalid draft key\");\n  await kv.delete(key);\n}\n\n// --- Admin session helpers ---\n\nconst SESSION_PREFIX = \"session:admin:\";\nconst SESSION_TTL_SEC = 60 * 60 * 24; // 24h\n\nfunction toBase64Url(bytes: Uint8Array): string {\n  let s = \"\";\n  for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);\n  return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nexport async function safeEqual(a: string, b: string): Promise<boolean> {\n  const enc = new TextEncoder();\n  const ha = new Uint8Array(await crypto.subtle.digest(\"SHA-256\", enc.encode(a)));\n  const hb = new Uint8Array(await crypto.subtle.digest(\"SHA-256\", enc.encode(b)));","sourceCodeStart":299,"sourceCodeEnd":335,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/web/lib/community-agent.ts#L299-L335","documentation":"deleteDraft validates the key with parseDraftKey before deleting from KV; if the key does not match `draft:<type>:<id>` (or its type is not a known AgentDraftType), it throws \"invalid draft key\". This stops arbitrary strings — including session or other namespace keys — from being deleted through the draft API.","triggerScenarios":"Calling deleteDraft(kv, key) with a key like 'session:admin:x', a bare id without the 'draft:type:' prefix, an unknown draft type segment, or an id containing an extra colon.","commonSituations":"Frontend sending the whole stored key including stale format; mixing up the session prefix with draft keys; an older client writing keys in a since-changed format; passing user input straight through as a key.","solutions":["Ensure the key was produced by draftKey() so it matches /^draft:<known-type>:<valid id>$/.","On the API boundary, validate the key with parseDraftKey first and return 400 for invalid keys instead of throwing.","If you only have type+id, rebuild the key via draftKey(type, id) before deleting.","Migrate any legacy-format keys stored in KV to the current draft: prefix scheme."],"exampleFix":"// before\nawait deleteDraft(kv, body.key); // may throw\n\n// after\nconst parsed = parseDraftKey(body.key);\nif (!parsed) return json({ error: 'invalid draft key' }, 400);\nawait deleteDraft(kv, body.key);","handlingStrategy":"validation","validationCode":"const parsed = parseDraftKey(key);\nif (!parsed) return json({ error: 'invalid draft key' }, 400);\nawait deleteDraft(kv, key);","typeGuard":"const isDraftKey = (key) => typeof key === 'string' && /^draft:(plan|<other-types>):[A-Za-z0-9._-]{1,128}$/.test(key);","tryCatchPattern":null,"preventionTips":["Always produce keys with draftKey(); never hand-assemble them.","Validate keys at the HTTP boundary and return 400, keeping deleteDraft's throw as a backstop.","Distinguish session keys from draft keys by prefix constant, never by convention.","Write an integration test asserting deleteDraft refuses non-draft prefixes."],"tags":["validation","kv","input"],"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"}