{"record":{"id":"a20bdcce0b0cac7f","repo":"paperclipai/paperclip","slug":"external-chat-card-kind-is-invalid","errorCode":null,"errorMessage":"External chat card kind is invalid","messagePattern":"External chat card kind is invalid","errorType":"validation","errorClass":"UnsafeChatPublicationError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-publication-projection.ts","lineNumber":309,"sourceCode":"    }\n    if (!seen.has(normalized)) {\n      seen.add(normalized);\n      output.push(normalized);\n    }\n  }\n  return output.length ? output : undefined;\n}\n\nfunction projectCard(\n  input: NonNullable<ChatPublicationProjectionInput[\"interaction\"]>,\n): { interactionId: string; card: SafeExternalChatCard } {\n  if (!SAFE_IDENTIFIER_RE.test(input.id)) {\n    throw new UnsafeChatPublicationError(\n      \"External chat interaction id is invalid\",\n    );\n  }\n  if (!CARD_KINDS.has(input.card.kind)) {\n    throw new UnsafeChatPublicationError(\"External chat card kind is invalid\");\n  }\n\n  const title = truncateByCodePoint(\n    projectSafeChatPublicationText(input.card.title),\n    MAX_TITLE_LENGTH,\n  );\n  const body = input.card.body\n    ? projectSafeChatPublicationText(input.card.body)\n    : undefined;\n  const rawActions = input.card.actions ?? [];\n  if (rawActions.length > MAX_CARD_ACTIONS) {\n    throw new UnsafeChatPublicationError(\n      `External chat cards support at most ${MAX_CARD_ACTIONS} actions`,\n    );\n  }\n\n  const actions: SafeExternalChatCardAction[] = [];\n  for (const action of rawActions) {","sourceCodeStart":291,"sourceCodeEnd":327,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/chat-publication-projection.ts#L291-L327","documentation":"projectCard checks interaction.card.kind against CARD_KINDS, the closed set of allowed card kinds: 'status', 'question', and 'confirmation' (chat-publication-projection.ts:67-71). Any other kind string throws UnsafeChatPublicationError because the provider-agnostic card schema (paperclip.chat.card.v1) only supports these kinds. The TypeScript type SafeExternalChatCardKind should prevent this statically, so the error fires on untyped or dynamically constructed input.","triggerScenarios":"Calling projectSafeChatPublication from JavaScript or untyped code with card.kind values like 'approval', 'info', 'warning', 'task', 'poll', or 'form'; a typo such as 'confirm' or 'statuses'; casing mismatches like 'Status'; or data loaded from a config file/API response without validation.","commonSituations":"An integration adds a new card style without extending SafeExternalChatCardKind and CARD_KINDS; a legacy payload uses an older kind name; a config-driven workflow reads 'kind' from YAML/JSON where 'question' was mistyped; a generic renderer maps its own categories directly onto card.kind.","solutions":["Use only the supported kinds: 'status', 'question', or 'confirmation'","Map your domain category to a supported kind before publication (e.g. 'approval' -> 'confirmation')","Fix casing/typos so the kind is an exact lowercase match to the allowed set","If a new kind is genuinely needed, extend SafeExternalChatCardKind in @paperclipai/shared AND the CARD_KINDS set in chat-publication-projection.ts together"],"exampleFix":"// before\ninteraction: { id, card: { kind: \"approval\", title, body } }\n// after\ninteraction: { id, card: { kind: \"confirmation\", title, body } } // 'confirmation' is in CARD_KINDS","handlingStrategy":"type-guard","validationCode":"const CARD_KINDS = new Set([\"status\", \"question\", \"confirmation\"] as const);\nif (!CARD_KINDS.has(card.kind)) throw new TypeError(`Unsupported card kind: ${String(card.kind)}`);","typeGuard":"function isSafeCardKind(kind: unknown): kind is \"status\" | \"question\" | \"confirmation\" {\n  return kind === \"status\" || kind === \"question\" || kind === \"confirmation\";\n}","tryCatchPattern":"try {\n  const payload = projectSafeChatPublication({ classification: \"external\", source, text, interaction });\n} catch (err) {\n  if (err instanceof UnsafeChatPublicationError && /card kind is invalid/.test(err.message)) {\n    console.error(\"card.kind must be one of status|question|confirmation\", { kind: interaction?.card?.kind });\n  }\n  throw err;\n}","preventionTips":["Type card inputs as SafeExternalChatCardKind from @paperclipai/shared so this fails at compile time","Map domain categories (approval, info) onto supported kinds before publication","Validate config-driven card definitions with the shared validator at load time","When adding a new kind, update SafeExternalChatCardKind and CARD_KINDS together and add a test"],"tags":["validation","enum","chat-card","unsupported-value"],"backgroundTag":"invalid-enum-value","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}