{"record":{"id":"0d82ebf55441291c","repo":"paperclipai/paperclip","slug":"external-chat-card-action-type-is-invalid","errorCode":null,"errorMessage":"External chat card action type is invalid","messagePattern":"External chat card action type is invalid","errorType":"validation","errorClass":"UnsafeChatPublicationError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-publication-projection.ts","lineNumber":353,"sourceCode":"          \"External chat action id is invalid\",\n        );\n      }\n      if (action.style && !CARD_ACTION_STYLES.has(action.style)) {\n        throw new UnsafeChatPublicationError(\n          \"External chat action style is invalid\",\n        );\n      }\n      actions.push({\n        type: \"callback\",\n        actionId: action.actionId,\n        label,\n        ...(action.style ? { style: action.style } : {}),\n      });\n      continue;\n    }\n\n    if (action.type !== \"link\") {\n      throw new UnsafeChatPublicationError(\n        \"External chat card action type is invalid\",\n      );\n    }\n\n    const url = sanitizeExternalChatUrl(action.url);\n    if (!url) continue;\n    actions.push({ type: \"link\", label, url });\n  }\n\n  return {\n    interactionId: input.id,\n    card: {\n      schema: \"paperclip.chat.card.v1\",\n      kind: input.card.kind,\n      title,\n      ...(body ? { body } : {}),\n      ...(actions.length ? { actions } : {}),\n    },","sourceCodeStart":335,"sourceCodeEnd":371,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/chat-publication-projection.ts#L335-L371","documentation":"Every card action must be either type \"callback\" or type \"link\". After the callback branch, projectCard checks whether the action is a \"link\"; if the type is anything else it throws UnsafeChatPublicationError rather than silently dropping the unknown action type. This guards the provider-bound payload against unrecognized or future/unmapped action kinds.","triggerScenarios":"Passing an action whose type is not \"callback\" or \"link\" — e.g. \"button\", \"select\", \"url\" (lowercase vs \"link\" is fine, but \"url\" is not), \"submit\", an empty string, or an unvalidated runtime value that bypassed the SafeExternalChatCardAction union type (e.g. parsed from JSON).","commonSituations":"Constructing actions from untyped JSON (API payload, DB row, plugin output) without validating the discriminated union; inventing a new action type and forgetting to add it to the projection; casing mismatches like \"Callback\".","solutions":["Use only type: \"callback\" (with actionId) or type: \"link\" (with url) when building card actions","Validate untyped input against the SafeExternalChatCardAction discriminated union (e.g. with a zod schema) before projecting","If a new action kind is genuinely needed, add it to the shared type and to projectCard's handling — do not pass it through untyped","Normalize aliases (e.g. \"url\" -> \"link\") at the ingestion boundary"],"exampleFix":"// before\nconst actions = raw.map((a) => ({ type: a.kind, ...a })); // kind may be \"url\" | \"button\" | ...\n// after\nconst actions = raw.map((a) => a.kind === \"url\"\n  ? { type: \"link\", label: a.label, url: a.url }\n  : { type: \"callback\", actionId: a.id, label: a.label });","handlingStrategy":"type-guard","validationCode":"function isValidActionType(t: unknown): boolean {\n  return t === \"callback\" || t === \"link\";\n}\ncard.actions = (card.actions ?? []).filter((a) => isValidActionType(a.type));","typeGuard":"function isSafeCardAction(a: unknown): a is SafeExternalChatCardAction {\n  if (typeof a !== \"object\" || a === null) return false;\n  const x = a as Record<string, unknown>;\n  if (x.type === \"callback\") return typeof x.actionId === \"string\" && typeof x.label === \"string\";\n  return x.type === \"link\" && typeof x.url === \"string\" && typeof x.label === \"string\";\n}","tryCatchPattern":"try {\n  const payload = projectSafeChatPublication(input);\n} catch (err) {\n  if (err instanceof UnsafeChatPublicationError && err.message === \"External chat card action type is invalid\") {\n    input.interaction.card.actions = input.interaction.card.actions.filter(isSafeCardAction);\n    return projectSafeChatPublication(input);\n  }\n  throw err;\n}","preventionTips":["Parse untrusted card JSON with a discriminated-union schema (zod) before projection","Never construct action objects with a computed type field from arbitrary strings","Keep the SafeExternalChatCardAction union as the single source of truth for action types"],"tags":["validation","chat-publication","discriminated-union"],"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"}