{"record":{"id":"06fe1d94a066e94c","repo":"Hmbown/CodeWhale","slug":"invalid-draft-id","errorCode":null,"errorMessage":"invalid draft id","messagePattern":"invalid draft id","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/lib/community-agent.ts","lineNumber":65,"sourceCode":"\nexport interface UsageLog {\n  date: string;\n  calls: number;\n  inputTokens: number;\n  outputTokens: number;\n}\n\nexport interface DeepSeekEnv {\n  baseUrl?: string;\n  model?: string;\n}\n\nconst AGENT_DRAFT_TYPE_SET = new Set<string>(AGENT_DRAFT_TYPES);\nconst DRAFT_ID_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;\n\nexport function draftKey(type: AgentDraftType, id: string): string {\n  if (!DRAFT_ID_PATTERN.test(id)) {\n    throw new Error(\"invalid draft id\");\n  }\n  return `draft:${type}:${id}`;\n}\n\nexport function parseDraftKey(key: string): { type: AgentDraftType; id: string } | null {\n  const match = /^draft:([^:]+):([^:]+)$/.exec(key);\n  if (!match || !AGENT_DRAFT_TYPE_SET.has(match[1]) || !DRAFT_ID_PATTERN.test(match[2])) {\n    return null;\n  }\n  return { type: match[1] as AgentDraftType, id: match[2] };\n}\n\nexport function isAgentDraft(value: unknown): value is AgentDraft {\n  if (!value || typeof value !== \"object\") return false;\n  const draft = value as Record<string, unknown>;\n  return (\n    typeof draft.id === \"string\" &&\n    DRAFT_ID_PATTERN.test(draft.id) &&","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/web/lib/community-agent.ts#L47-L83","documentation":"draftKey() builds KV keys of the form draft:<type>:<id> and refuses ids that fail /^[A-Za-z0-9._-]{1,128}$/ - empty, longer than 128 chars, or containing characters (colons, spaces, slashes, unicode, base64 '+'/'=') that would corrupt the key format or break parseDraftKey() round-tripping.","triggerScenarios":"Passing user-supplied titles, URLs, or free text as the draft id; ids containing ':' which would shift key segments; an id that is empty after trimming.","commonSituations":"Using the chat subject or issue title directly as the id; base64-encoded ids; copy-paste ids with a trailing newline.","solutions":["Generate ids from a safe alphabet, e.g. crypto.randomUUID() (hyphens are allowed)","Slugify free text before use: lowercase, replace [^a-z0-9._-]+ with '-', slice to 128","Validate with the same regex in the UI and reject early with a friendly message"],"exampleFix":"// before\nconst key = draftKey('dispatch', rawTitle);\n\n// after\nconst id = rawTitle.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 128);\nif (!id) throw new Error('draft id collapsed to empty after slugify');\nconst key = draftKey('dispatch', id);","handlingStrategy":"type-guard","validationCode":"const DRAFT_ID = /^[A-Za-z0-9._-]{1,128}$/;\nif (!DRAFT_ID.test(id)) {\n  id = id.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').slice(0, 128) || crypto.randomUUID();\n}","typeGuard":"const DRAFT_ID_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;\nfunction isDraftId(id) {\n  return typeof id === 'string' && DRAFT_ID_PATTERN.test(id);\n}\n// usage: if (!isDraftId(id)) return badRequest('invalid draft id');","tryCatchPattern":null,"preventionTips":["Generate ids with crypto.randomUUID() or slugs, never free text","Reject invalid ids at the API boundary with a 400 before they reach draftKey()","Keep the id pattern in one shared constant so client and server validate identically"],"tags":["validation","kv-storage","cloudflare-workers","ids"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}