{"record":{"id":"7c3b681c4a51e921","repo":"paperclipai/paperclip","slug":"external-chat-action-id-is-invalid","errorCode":null,"errorMessage":"External chat action id is invalid","messagePattern":"External chat action id is invalid","errorType":"validation","errorClass":"UnsafeChatPublicationError","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-publication-projection.ts","lineNumber":334,"sourceCode":"  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) {\n    const label = truncateByCodePoint(\n      projectSafeChatPublicationText(action.label),\n      MAX_ACTION_LABEL_LENGTH,\n    );\n    if (action.type === \"callback\") {\n      if (!SAFE_IDENTIFIER_RE.test(action.actionId)) {\n        throw new UnsafeChatPublicationError(\n          \"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\") {","sourceCodeStart":316,"sourceCodeEnd":352,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/chat-publication-projection.ts#L316-L352","documentation":"For callback-type card actions, projectCard validates action.actionId against SAFE_IDENTIFIER_RE (/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/). A callback actionId is the opaque key echoed back when the user clicks the button, so it must be a short, safe identifier; anything empty, starting with a non-alphanumeric character, or containing whitespace, quotes, slashes, or other special characters (or longer than 160 chars) is rejected.","triggerScenarios":"Passing a callback action with actionId that is null/undefined at runtime, empty, starts with a symbol (e.g. \".foo\", \"-foo\"), contains spaces or slashes (e.g. \"issue/123 confirm\"), has non-ASCII characters, or exceeds 160 characters.","commonSituations":"Using a raw URL path segment or a sentence as the actionId; embedding a UUID with braces; passing user-controlled text as the actionId; forgetting to slugify a label used as an identifier; a type-level mismatch where actionId is optional but the code treats it as required.","solutions":["Generate actionIds as short slugs matching /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/ (e.g. \"confirm-task-123\"), not free-form text or URLs","Encode structured data into the ID safely (base64url or a slugified key) instead of raw values with spaces/slashes","Trim and sanitize the actionId at card-construction time; drop actions with invalid ids before projection","Test the id with SAFE_IDENTIFIER_RE before building the callback action"],"exampleFix":"// before\n{ type: \"callback\", actionId: `confirm ${task.title}`, label: \"Confirm\" }\n// after\nconst slug = `confirm-task-${task.id}`.replace(/[^A-Za-z0-9_.:-]/g, \"-\");\nif (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/.test(slug)) throw new Error(\"bad actionId\");\n{ type: \"callback\", actionId: slug, label: \"Confirm\" }","handlingStrategy":"validation","validationCode":"const SAFE_IDENTIFIER_RE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/;\nif (typeof action.actionId !== \"string\" || !SAFE_IDENTIFIER_RE.test(action.actionId)) {\n  throw new Error(`Invalid callback actionId: ${action.actionId}`);\n}","typeGuard":"const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/;\nfunction isSafeActionId(v: unknown): v is string {\n  return typeof v === \"string\" && SAFE_ID_RE.test(v);\n}","tryCatchPattern":"try {\n  const payload = projectSafeChatPublication(input);\n} catch (err) {\n  if (err instanceof UnsafeChatPublicationError && err.message === \"External chat action id is invalid\") {\n    logger.error(\"callback actionId failed SAFE_IDENTIFIER_RE\", { card: input.interaction.card });\n    return null; // skip the publication or rebuild with slugified ids\n  }\n  throw err;\n}","preventionTips":["Always generate actionIds from structured keys (ids, enums), never free text or URLs","Slugify any user-derived component: replace(/[^A-Za-z0-9_.:-]/g, \"-\") and trim leading non-alphanumerics","Validate the full card with a zod schema before calling the projection API"],"tags":["validation","chat-publication","identifier-format"],"backgroundTag":"invalid-identifier-format","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"}