bitsnap-react 1.22.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/AGENTS.md +20 -0
  2. package/dist/BitsnapCart-GK3MQGP7.js +2 -0
  3. package/dist/BitsnapCart-GK3MQGP7.js.map +1 -0
  4. package/dist/BitsnapCart-ZC6LMXLO.mjs +2 -0
  5. package/dist/CartProvider-FJDBDV4N.js +2 -0
  6. package/dist/CartProvider-FJDBDV4N.js.map +1 -0
  7. package/dist/{CartProvider-YUV2Q523.mjs → CartProvider-P7BZVK2M.mjs} +2 -2
  8. package/dist/PanelModelFiles-EKQRLF46.js.map +1 -1
  9. package/dist/analytics.js +1 -1
  10. package/dist/analytics.js.map +1 -1
  11. package/dist/analytics.mjs +1 -1
  12. package/dist/catalog.js +1 -1
  13. package/dist/catalog.js.map +1 -1
  14. package/dist/catalog.mjs +1 -1
  15. package/dist/chat.js +1 -1
  16. package/dist/chat.js.map +1 -1
  17. package/dist/chat.mjs +1 -1
  18. package/dist/{chunk-SAKZOHU7.js → chunk-2CL4B4KM.js} +2 -2
  19. package/dist/chunk-2CL4B4KM.js.map +1 -0
  20. package/dist/chunk-2D2TQK3R.js +2 -0
  21. package/dist/chunk-2D2TQK3R.js.map +1 -0
  22. package/dist/{chunk-C2AJ5B32.mjs → chunk-4AU5SEHU.mjs} +2 -2
  23. package/dist/chunk-6Q3VGPSF.js.map +1 -1
  24. package/dist/{chunk-ZFZR3CI5.js → chunk-7GSQV4NW.js} +2 -2
  25. package/dist/chunk-7GSQV4NW.js.map +1 -0
  26. package/dist/{chunk-WYRECFVW.js → chunk-CA5EJJW7.js} +2 -2
  27. package/dist/chunk-CA5EJJW7.js.map +1 -0
  28. package/dist/{chunk-U37ZH4YJ.js → chunk-DCICRN6G.js} +2 -2
  29. package/dist/chunk-DCICRN6G.js.map +1 -0
  30. package/dist/{chunk-QQSCVPOG.mjs → chunk-EVV25IKM.mjs} +2 -2
  31. package/dist/{chunk-QQSCVPOG.mjs.map → chunk-EVV25IKM.mjs.map} +1 -1
  32. package/dist/{chunk-MBSXTB6G.js → chunk-EVX5SQL2.js} +2 -2
  33. package/dist/chunk-EVX5SQL2.js.map +1 -0
  34. package/dist/chunk-FVZ4W7QG.js +5 -0
  35. package/dist/chunk-FVZ4W7QG.js.map +1 -0
  36. package/dist/chunk-GMQYQTUM.mjs +2 -0
  37. package/dist/chunk-GMQYQTUM.mjs.map +1 -0
  38. package/dist/{chunk-35ZQU353.js → chunk-I6QN3T6K.js} +2 -2
  39. package/dist/chunk-I6QN3T6K.js.map +1 -0
  40. package/dist/chunk-ILYJCT5S.js +2 -0
  41. package/dist/chunk-ILYJCT5S.js.map +1 -0
  42. package/dist/{chunk-QJOMFK4X.mjs → chunk-JRSQ2IGQ.mjs} +2 -2
  43. package/dist/chunk-JSDGZ4NY.js.map +1 -1
  44. package/dist/chunk-KOYBZORL.js.map +1 -1
  45. package/dist/{chunk-5MXBUGY3.mjs → chunk-M6QFQ3DR.mjs} +2 -2
  46. package/dist/chunk-MDAFPGPA.js.map +1 -1
  47. package/dist/{chunk-KFY6IOAA.js → chunk-MY6BPCRV.js} +2 -2
  48. package/dist/{chunk-ZFUE3AX5.mjs.map → chunk-MY6BPCRV.js.map} +1 -1
  49. package/dist/{chunk-QCIETNFZ.mjs → chunk-NMXTTD5M.mjs} +2 -2
  50. package/dist/chunk-PFKZ7KUU.js.map +1 -1
  51. package/dist/chunk-Q337IQNA.js.map +1 -1
  52. package/dist/{chunk-IVB3E4V3.mjs → chunk-RA6M3TJ3.mjs} +2 -2
  53. package/dist/{chunk-NCCP2UDM.mjs → chunk-SKQJESEB.mjs} +2 -2
  54. package/dist/chunk-STDV54FL.mjs +2 -0
  55. package/dist/chunk-STDV54FL.mjs.map +1 -0
  56. package/dist/{chunk-JS3KK56H.js → chunk-SZLSKIYU.js} +2 -2
  57. package/dist/chunk-SZLSKIYU.js.map +1 -0
  58. package/dist/chunk-W3RGQYPD.js.map +1 -1
  59. package/dist/{chunk-TVZLDLAV.mjs → chunk-W4EUYPJ3.mjs} +2 -2
  60. package/dist/chunk-Z3YOJ6NZ.js.map +1 -1
  61. package/dist/chunk-ZMEWUEZJ.mjs +5 -0
  62. package/dist/chunk-ZMEWUEZJ.mjs.map +1 -0
  63. package/dist/consent.js.map +1 -1
  64. package/dist/course.js +1 -1
  65. package/dist/course.js.map +1 -1
  66. package/dist/course.mjs +1 -1
  67. package/dist/embed/analytics.js +1 -1
  68. package/dist/errors.js.map +1 -1
  69. package/dist/index.css +1 -1
  70. package/dist/index.css.map +1 -1
  71. package/dist/index.js +1 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/index.mjs +1 -1
  74. package/dist/index.mjs.map +1 -1
  75. package/dist/internal.js.map +1 -1
  76. package/dist/methods-IEHZTZEY.mjs +2 -0
  77. package/dist/methods-S4KBRY2I.js +2 -0
  78. package/dist/methods-S4KBRY2I.js.map +1 -0
  79. package/dist/model-files-client-P4PORZJ6.js.map +1 -1
  80. package/dist/models.js.map +1 -1
  81. package/dist/returns.js +1 -1
  82. package/dist/returns.js.map +1 -1
  83. package/dist/returns.mjs +1 -1
  84. package/dist/viewer3d.js.map +1 -1
  85. package/dist/viewer3d.mjs +1 -1
  86. package/package.json +2 -2
  87. package/dist/BitsnapCart-AINUJQVF.mjs +0 -2
  88. package/dist/BitsnapCart-GP2UJSG4.js +0 -2
  89. package/dist/BitsnapCart-GP2UJSG4.js.map +0 -1
  90. package/dist/CartProvider-HTKGBPVA.js +0 -2
  91. package/dist/CartProvider-HTKGBPVA.js.map +0 -1
  92. package/dist/chunk-35ZQU353.js.map +0 -1
  93. package/dist/chunk-3TUL63IC.js +0 -2
  94. package/dist/chunk-3TUL63IC.js.map +0 -1
  95. package/dist/chunk-4JR74NXR.js +0 -2
  96. package/dist/chunk-4JR74NXR.js.map +0 -1
  97. package/dist/chunk-GLKBCVNH.mjs +0 -2
  98. package/dist/chunk-GLKBCVNH.mjs.map +0 -1
  99. package/dist/chunk-JS3KK56H.js.map +0 -1
  100. package/dist/chunk-KFY6IOAA.js.map +0 -1
  101. package/dist/chunk-MBSXTB6G.js.map +0 -1
  102. package/dist/chunk-QJBQ45R6.mjs +0 -5
  103. package/dist/chunk-QJBQ45R6.mjs.map +0 -1
  104. package/dist/chunk-SAKZOHU7.js.map +0 -1
  105. package/dist/chunk-SZNTXRFD.js +0 -5
  106. package/dist/chunk-SZNTXRFD.js.map +0 -1
  107. package/dist/chunk-U37ZH4YJ.js.map +0 -1
  108. package/dist/chunk-WYRECFVW.js.map +0 -1
  109. package/dist/chunk-ZFUE3AX5.mjs +0 -2
  110. package/dist/chunk-ZFZR3CI5.js.map +0 -1
  111. package/dist/methods-4Q37ITIM.js +0 -2
  112. package/dist/methods-4Q37ITIM.js.map +0 -1
  113. package/dist/methods-JLVRTNPE.mjs +0 -2
  114. /package/dist/{BitsnapCart-AINUJQVF.mjs.map → BitsnapCart-ZC6LMXLO.mjs.map} +0 -0
  115. /package/dist/{CartProvider-YUV2Q523.mjs.map → CartProvider-P7BZVK2M.mjs.map} +0 -0
  116. /package/dist/{chunk-C2AJ5B32.mjs.map → chunk-4AU5SEHU.mjs.map} +0 -0
  117. /package/dist/{chunk-QJOMFK4X.mjs.map → chunk-JRSQ2IGQ.mjs.map} +0 -0
  118. /package/dist/{chunk-5MXBUGY3.mjs.map → chunk-M6QFQ3DR.mjs.map} +0 -0
  119. /package/dist/{chunk-QCIETNFZ.mjs.map → chunk-NMXTTD5M.mjs.map} +0 -0
  120. /package/dist/{chunk-IVB3E4V3.mjs.map → chunk-RA6M3TJ3.mjs.map} +0 -0
  121. /package/dist/{chunk-NCCP2UDM.mjs.map → chunk-SKQJESEB.mjs.map} +0 -0
  122. /package/dist/{chunk-TVZLDLAV.mjs.map → chunk-W4EUYPJ3.mjs.map} +0 -0
  123. /package/dist/{methods-JLVRTNPE.mjs.map → methods-IEHZTZEY.mjs.map} +0 -0
package/dist/chat.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/emems/.t3/worktrees/bitsnap-react/emems-chat-payment-requests/dist/chat.js","../src/chat/widgets.tsx","../src/chat/buy.ts","../src/chat/types.ts","../src/chat/client.ts"],"names":["addPackToCart","input","addProductToCart","DEFAULT_CHAT_API_URL","DEFAULT_CHAT_ATTACHMENTS_URL","DEFAULT_CHAT_REALTIME_URL","ChatError","code","status","data","retry","createChatClient","options","baseUrl","localOrigin","attachmentBase","doFetch","args","call","method","path","body","response"],"mappings":"AAAA,y/BAAwC,sDAAyC,sDAAyC,sDAAiD,+BAA4B,sDAAwC,+BAA4B,+BAA4B,sDAAgD,+BAA4B,8BCA9T,MCqBrD,SAAsBA,EAAAA,CAAcC,CAAAA,CAAoC,CACtE,MAAMC,gCAAAA,CAEF,EAAA,CAAID,CAAAA,CAAM,SAAA,CACV,IAAA,kBAAMA,CAAAA,CAAM,IAAA,SAAQ,IAAA,CACpB,KAAA,kBAAOA,CAAAA,CAAM,UAAA,SAAc,GAAA,CAC3B,QAAA,CAAU,EAAA,CACV,MAAA,CAAQA,CAAAA,CAAM,MAChB,CAAA,CACA,CAAE,SAAA,CAAWA,CAAAA,CAAM,SAAA,CAAW,IAAA,CAAMA,CAAAA,CAAM,IAAA,CAAM,QAAA,CAAU,CAAA,CAAK,CACjE,CACF,CCzBO,IAAME,EAAAA,CAAuB,yBAAA,CAEvBC,EAAAA,wCAA+B,oBAAA,CAG/BC,EAAAA,qCACX,oDAAA,CCcK,IAAMC,CAAAA,CAAN,MAAA,QAAwB,KAAM,CAC1B,WAOT,CACEC,CAAAA,CACAC,CAAAA,CACAC,CAAAA,CAAgC,CAAC,CAAA,CACjC,CACA,KAAA,CAAM,CAAA,MAAA,EAASF,CAAI,CAAA,EAAA,EAAKC,CAAM,CAAA,CAAA,CAAG,CAAA,CACjC,IAAA,CAAK,IAAA,CAAO,WAAA,CACZ,IAAA,CAAK,IAAA,CAAOD,CAAAA,CACZ,IAAA,CAAK,MAAA,CAASC,CAAAA,CACd,IAAA,CAAK,IAAA,CAAOC,CAAAA,CACZ,IAAMC,CAAAA,CAAQ,MAAA,CAAOD,CAAAA,CAAK,WAAW,CAAA,CACjC,MAAA,CAAO,QAAA,CAASC,CAAK,CAAA,EAAKA,CAAAA,CAAQ,CAAA,EAAA,CAAG,IAAA,CAAK,UAAA,CAAaA,CAAAA,CAC7D,CACF,CAAA,CAmDO,SAASC,EAAAA,CAAiBC,CAAAA,CAAwC,CACvE,IAAMC,CAAAA,CAAAA,CAAWD,CAAAA,CAAQ,OAAA,EAAWT,EAAAA,CAAAA,CAAsB,OAAA,CAAQ,MAAA,CAAQ,EAAE,CAAA,CACtEW,CAAAA,CAAc,OAAO,MAAA,CAAW,GAAA,EAAe,CAAC,WAAA,CAAa,WAAW,CAAA,CAAE,QAAA,CAAS,MAAA,CAAO,QAAA,CAAS,QAAQ,CAAA,CAAI,MAAA,CAAO,QAAA,CAAS,MAAA,CAAS,KAAA,CAAA,CACxIC,CAAAA,CAAAA,mCAAkBH,CAAAA,CAAQ,cAAA,SAAkBE,GAAAA,SAAeV,IAAAA,CAAAA,CAA8B,OAAA,CAAQ,MAAA,CAAQ,EAAE,CAAA,CAC3GY,CAAAA,kBAAUJ,CAAAA,CAAQ,KAAA,SAAA,CAAU,CAAA,GAAIK,CAAAA,CAAAA,EAAS,KAAA,CAAM,GAAGA,CAAI,CAAA,GAAA,CAE5D,MAAA,SAAeC,CAAAA,CACbC,CAAAA,CACAC,CAAAA,CACAC,CAAAA,CAC4C,CAC5C,IAAIC,CAAAA,CACJ,GAAI,CACFA,CAAAA,CAAW,MAAMN,CAAAA,CAAQH,CAAAA,CAAUO,CAAAA,CAAM,CACvC,MAAA,CAAAD,CAAAA,CACA,OAAA,CAAS,CACP,aAAA,CAAe,CAAA,OAAA,EAAUP,CAAAA,CAAQ,KAAK,CAAA,CAAA","file":"/home/emems/.t3/worktrees/bitsnap-react/emems-chat-payment-requests/dist/chat.js","sourcesContent":[null,"import { useEffect, useMemo, useRef, useState } from \"react\";\nimport { getLocale } from \"../components/checkout/locale\";\nimport { getPanelConfig } from \"../panel/config\";\nimport { getPanelSession } from \"../panel/session-storage\";\nimport { addPackToCart } from \"./buy\";\nimport { createChatClient, type ChatClient } from \"./client\";\nimport { ConversationView } from \"./Conversation\";\nimport type { NatsModule } from \"./realtime\";\nimport { getChatTexts, type ChatTexts } from \"./texts\";\nimport { useConversation, type ConversationLock } from \"./useConversation\";\n\n/** What the lock's buy action gets. */\nexport type ChatBuyRequest = {\n /** The product (or variant) to sell: `buyProductId`, else the locked product. */\n productId: string;\n /** `replies`, `expired`, `no-pack` or `no-access`. */\n reason: ConversationLock[\"reason\"];\n /** Known for a locked channel (from the chat service). */\n priceCents?: number;\n amount?: number;\n email?: string;\n code?: string;\n turnstileToken?: string;\n};\n\n/** Props shared by `ChatThread` and `Channel`. */\nexport type ChatWidgetBaseProps = {\n /**\n * Public Bitsnap project id of the shop. The cart uses it, the panel\n * session is looked up with it, and it is the `X-Bitsnap-Project` of the\n * chat service unless `chatProjectID` is given.\n */\n projectID: string;\n /** Project id for the chat service, when it differs from `projectID`. */\n chatProjectID?: string;\n /**\n * Bearer for the chat service: a `bcht_` buyer token from your server\n * (`POST /api/v1/chat/buyer-token`). Defaults to the customer panel session\n * of this browser (after `PanelLogin`).\n */\n token?: string;\n /** Chat service base URL. Defaults to `https://chat.bitsnap.pl`. */\n apiUrl?: string;\n /** Bitsnap app host serving private chat attachment APIs (defaults to the page origin). */\n attachmentsUrl?: string;\n /** NATS WebSocket URL overriding the one of the realtime token. */\n realtimeUrl?: string;\n /** Bitsnap host of the panel session (`setHost`), when not the default. */\n host?: string;\n /** Polling interval in ms while the live connection is down. Default 8000. */\n pollIntervalMs?: number;\n /** Language of the default texts; defaults to the library locale. */\n locale?: string;\n /** Text overrides, whatever the locale. */\n texts?: Partial<ChatTexts>;\n /** Heading of the conversation. */\n title?: string;\n /** Shows a back button in the header. */\n onBack?: () => void;\n /**\n * The lock's \"Kup pakiet\" / \"Kup dostęp\". Default: add the product to the\n * page cart and open it (mount `BitsnapCheckout` on the page). Throw to\n * show the error line.\n */\n onBuy?: (request: ChatBuyRequest) => Promise<void> | void;\n /** The product or variant the buy action sells (e.g. the pack variant). */\n buyProductId?: string;\n className?: string;\n /** A ready client (tests, custom transport). Replaces token and URLs. */\n client?: ChatClient;\n /** @internal Replaces the NATS loader (tests). */\n loadNats?: () => Promise<NatsModule>;\n};\n\nexport type ChatThreadProps = ChatWidgetBaseProps & {\n /** The service product of the pack: opens its thread, creating it if needed. */\n productId?: string;\n /** An existing thread. Give this or `productId`. */\n threadId?: string;\n};\n\nexport type ChannelProps = ChatWidgetBaseProps & {\n /** The channel (its id is also the id of its conversation). */\n channelId: string;\n /** The community product sold by the lock; defaults to the channel's own. */\n productId?: string;\n};\n\n/**\n * The chat client of a widget. The panel session lives in localStorage, so\n * it is read after mounting (`null` until then: the widget stays loading and\n * the server HTML has no session-dependent content).\n */\nfunction useWidgetClient(props: ChatWidgetBaseProps): ChatClient | null | undefined {\n const [stored, setStored] = useState<string | undefined | null>(null);\n const host = props.host ?? getPanelConfig().host;\n useEffect(() => {\n if (props.client || props.token) return;\n setStored(getPanelSession(props.projectID, host));\n }, [props.client, props.token, props.projectID, host]);\n const token = props.token ?? stored;\n const projectId = props.chatProjectID ?? props.projectID;\n return useMemo(() => {\n if (props.client) return props.client;\n if (token === null) return null;\n if (!token) return undefined;\n return createChatClient({ projectId, token, baseUrl: props.apiUrl, attachmentsUrl: props.attachmentsUrl });\n }, [props.client, token, projectId, props.apiUrl, props.attachmentsUrl]);\n}\n\nfunction useBuy(props: ChatWidgetBaseProps) {\n const claim = useRef<{ key: string; token: string; expires: number } | null>(null);\n return async (lock: ConversationLock & { email?: string; code?: string; turnstileToken?: string }) => {\n const productId = props.buyProductId ?? lock.productId;\n if (!productId) return;\n const request: ChatBuyRequest = { productId, reason: lock.reason, priceCents: lock.priceCents, ...(lock.amount != null ? { amount: lock.amount } : {}), ...(lock.email ? { email: lock.email } : {}), ...(lock.code ? { code: lock.code } : {}), ...(lock.turnstileToken != null ? { turnstileToken: lock.turnstileToken } : {}) };\n if (props.onBuy) {\n await props.onBuy(request);\n return;\n }\n const commerceBase = (props.host ?? getPanelConfig().host ?? \"https://bitsnap.pl\").replace(/\\/+$/, \"\");\n const isFreeClaim = lock.freeClaimLimit != null && (lock.priceType === \"free\" || (lock.priceType === \"pay_what_you_want\" && lock.minimumCents === 0 && request.amount === 0));\n if (isFreeClaim && request.email && request.turnstileToken != null) {\n claim.current = null;\n const response = await fetch(`${commerceBase}/api/integrations/${encodeURIComponent(props.projectID)}/public-commerce/free-claim/request-code`, {\n method: \"POST\", headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ email: request.email, turnstileToken: request.turnstileToken }),\n });\n const result = await response.json() as { code?: string };\n if (!response.ok) throw new Error(result.code ?? \"internal\");\n return;\n }\n if (isFreeClaim && request.email && request.code) {\n const key = JSON.stringify([commerceBase, props.projectID, productId, request.email, request.code]);\n if (!claim.current || claim.current.key !== key || claim.current.expires <= Date.now()) {\n claim.current = null;\n const verify = await fetch(`${commerceBase}/api/integrations/${encodeURIComponent(props.projectID)}/public-commerce/free-claim/verify-code`, {\n method: \"POST\", headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ email: request.email, code: request.code }),\n });\n const verified = await verify.json() as { claimToken?: string; code?: string; expiresIn?: number };\n if (!verify.ok || !verified.claimToken) throw new Error(verified.code ?? \"email-verification-required\");\n claim.current = { key, token: verified.claimToken, expires: Date.now() + (verified.expiresIn ?? 600) * 1000 };\n }\n const bought = await fetch(`${commerceBase}/api/integrations/${encodeURIComponent(props.projectID)}/public-commerce/buy`, {\n method: \"POST\", headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ items: [{ id: productId, quantity: 1, amount: 0 }], details: { email: request.email }, claimToken: claim.current.token }),\n });\n const result = await bought.json() as { success?: boolean; url?: string; code?: string };\n if (!bought.ok || !result.url) throw new Error(result.code ?? \"free-order-not-supported\");\n window.location.assign(result.url);\n return;\n }\n await addPackToCart({\n projectID: props.projectID,\n productId,\n name: props.title,\n priceCents: lock.priceCents,\n amount: lock.amount,\n host: props.host,\n });\n };\n}\n\n/**\n * A buyer's pack thread for one service product: history, live updates,\n * sending with pending and refused states, report and mute, and the lock\n * with \"Kup pakiet\" when the replies are used up or the pack expired.\n * Load `bitsnap-react/dist/index.css` once.\n */\nexport function ChatThread(props: ChatThreadProps) {\n const client = useWidgetClient(props);\n const locale = props.locale ?? getLocale();\n const texts = useMemo(() => getChatTexts(props.texts, locale), [props.texts, locale]);\n const conversation = useConversation({\n client,\n target: { kind: \"thread\", threadId: props.threadId, productId: props.productId },\n pollIntervalMs: props.pollIntervalMs,\n realtimeUrl: props.realtimeUrl,\n loadNats: props.loadNats,\n });\n const buy = useBuy(props);\n return (\n <div className=\"bitsnap-react h-full\">\n <ConversationView\n host={props.host}\n conversation={conversation}\n texts={texts}\n locale={locale}\n kind=\"thread\"\n title={props.title}\n onBack={props.onBack}\n onBuy={buy}\n className={props.className}\n />\n </div>\n );\n}\n\n/**\n * A community channel: messages, live updates, posting when the channel\n * allows it (with the channel's moderation refusals shown on the message),\n * report, and the lock with \"Kup dostęp\" for a buyer without a grant.\n * Load `bitsnap-react/dist/index.css` once.\n */\nexport function Channel(props: ChannelProps) {\n const client = useWidgetClient(props);\n const locale = props.locale ?? getLocale();\n const texts = useMemo(() => getChatTexts(props.texts, locale), [props.texts, locale]);\n const conversation = useConversation({\n client,\n target: { kind: \"channel\", channelId: props.channelId, productId: props.productId },\n pollIntervalMs: props.pollIntervalMs,\n realtimeUrl: props.realtimeUrl,\n loadNats: props.loadNats,\n });\n const buy = useBuy(props);\n return (\n <div className=\"bitsnap-react h-full\">\n <ConversationView\n host={props.host}\n conversation={conversation}\n texts={texts}\n locale={locale}\n kind=\"channel\"\n title={props.title}\n onBack={props.onBack}\n onBuy={buy}\n className={props.className}\n />\n </div>\n );\n}\n","import { addProductToCart } from \"../components/products/add-to-cart\";\n\nexport type BuyPackInput = {\n /** Public project id of the shop (the cart's project). */\n projectID: string;\n /** The product (or variant) to add: the pack's service or community product. */\n productId: string;\n name?: string;\n priceCents?: number;\n amount?: number;\n /** Backend origin of the cart, when not the default. */\n host?: string;\n};\n\n/**\n * Default \"Kup pakiet\" / \"Kup dostęp\" action: adds the product to the page\n * cart (`Bitsnap.addProductToCart`) and opens the cart drawer. The page needs\n * a mounted `BitsnapCheckout`. When the page cart belongs to another project\n * or host nothing changes and the promise rejects; pass `onBuy` to the widget\n * to use your own checkout instead.\n */\nexport async function addPackToCart(input: BuyPackInput): Promise<void> {\n await addProductToCart(\n {\n id: input.productId,\n name: input.name ?? \"\",\n price: input.priceCents ?? 0,\n currency: \"\",\n amount: input.amount,\n },\n { projectID: input.projectID, host: input.host, showCart: true },\n );\n}\n","/**\n * Buyer side of the Bitsnap chat service (`bitsnap-messaging`): the shapes\n * the library works with. The wire is snake_case JSON; `client.ts` maps it to\n * these camelCase types. Times are ISO 8601 strings, `null` when absent.\n */\n\n/** Default base URL of the chat service. */\nexport const DEFAULT_CHAT_API_URL = \"https://chat.bitsnap.pl\";\n/** Bitsnap API that authenticates callers before signing private attachment URLs. */\nexport const DEFAULT_CHAT_ATTACHMENTS_URL = \"https://bitsnap.pl\";\n\n/** Default NATS WebSocket URL, used when the realtime token lists no server. */\nexport const DEFAULT_CHAT_REALTIME_URL =\n \"wss://bitsnap-nats-queue-production.up.railway.app\";\n\n/** One paid order that gives a pack thread creator replies. */\nexport type ChatPack = {\n orderId: string;\n productId?: string;\n replies: number;\n used: number;\n left: number;\n expiresAt: string | null;\n expired: boolean;\n};\n\n/** A stored message, or the pending answer of a send. */\nexport type ChatMessage = {\n id: string;\n threadId: string;\n authorEmail: string;\n body: string;\n /** Missing on older servers; unknown wire kinds are rendered as text. */\n kind: \"text\" | \"payment_request\";\n paymentRequest?: ChatPaymentRequest;\n /** The wire kind was payment_request but its snapshot failed validation. */\n paymentRequestInvalid?: boolean;\n attachmentKey?: string;\n attachmentKind?: string;\n attachmentName?: string;\n attachmentSize?: number;\n attachmentContentType?: string;\n createdAt: string;\n pinned: boolean;\n /** Soft deleted: the body is empty. */\n deleted: boolean;\n /** A message of the service, such as the creator's away notice. */\n system: boolean;\n /** `\"pending\"` in the answer of a send: queued, not stored yet. */\n status?: \"pending\";\n};\n\n/** Immutable buyer-visible snapshot attached to a chat payment request. */\nexport type ChatPaymentRequest = {\n requestId: string;\n title: string;\n description: string;\n amountMode: \"exact\" | \"pwyw\";\n amountMinor: number;\n minAmountMinor: number | null;\n maxAmountMinor: number | null;\n currency: string;\n extraReplies: number;\n extendExpiryDays: number;\n paymentUrl: string;\n status: \"pending\" | \"paid\" | \"expired\" | \"cancelled\";\n expiresAt: string;\n paidAt: string | null;\n};\n\nexport type ChatAttachment = {\n key: string;\n kind: \"image\" | \"file\";\n filename: string;\n size: number;\n contentType: string;\n};\n\n/** The state of a conversation for the caller (`ThreadView` on the wire). */\nexport type ChatThreadSummary = {\n id: string;\n kind: \"pack\" | \"channel\";\n channelId?: string;\n /** For a buyer, the creator's e-mail (pack) or the channel name. */\n title: string;\n lastMessage?: ChatMessage;\n unread: number;\n repliesTotal: number;\n repliesUsed: number;\n repliesLeft: number;\n expiresAt: string | null;\n /** The caller cannot write: every pack has expired. */\n composerLocked: boolean;\n /** The creator blocked this buyer from posting. */\n blocked?: boolean;\n productId?: string;\n /** The creator's promise in hours (\"odpowiedź w ciągu 48 h\"), 0 for none. */\n responseHours: number;\n packs: ChatPack[];\n muted: boolean;\n};\n\n/** A channel the caller may read. */\nexport type ChatChannelSummary = {\n id: string;\n name: string;\n topic: string;\n welcome: string;\n whoCanPost: \"creator\" | \"members\";\n productId: string;\n /** `creator`, `member` or `muted`. */\n role: string;\n memberCount: number;\n createdAt: string;\n blockLinks: boolean;\n blockMedia: boolean;\n cooldownSeconds: number;\n};\n\n/** A pack the buyer holds and has not opened a thread for yet. */\nexport type ChatPackEntitlement = {\n productId: string;\n name: string;\n repliesLeftHint: number;\n expiresAt: string | null;\n};\n\n/** A community product the buyer holds; `channelId` once a channel is bound. */\nexport type ChatChannelEntitlement = {\n productId: string;\n channelId?: string;\n};\n\n/** `GET /v1/me`: the buyer's chat in one project. */\nexport type ChatMe = {\n email: string;\n threads: ChatThreadSummary[];\n entitlements: {\n packs: ChatPackEntitlement[];\n channels: ChatChannelEntitlement[];\n };\n channels: ChatChannelSummary[];\n};\n\n/** One page of history, newest first as the service answers it. */\nexport type ChatMessagePage = {\n messages: ChatMessage[];\n hasMore: boolean;\n};\n\n/** The thread part of a send answer. */\nexport type ChatThreadState = Pick<\n ChatThreadSummary,\n | \"id\"\n | \"kind\"\n | \"repliesTotal\"\n | \"repliesUsed\"\n | \"repliesLeft\"\n | \"expiresAt\"\n | \"composerLocked\"\n | \"responseHours\"\n>;\n\n/** `POST /v1/threads/{id}/messages` answers 202 with the queued message. */\nexport type ChatSendResult = {\n message: ChatMessage;\n thread: ChatThreadState;\n};\n\nexport type ChatReportReason = \"spam\" | \"abuse\" | \"illegal\" | \"other\";\n\n/** `POST /v1/realtime/token`. */\nexport type ChatRealtimeToken = {\n jwt: string;\n expiresAt: string;\n servers: string[];\n subjects: string[];\n truncated: boolean;\n};\n\n/** Live events on the thread or channel subject. */\nexport type ChatRealtimeEvent =\n | { type: \"message.created\"; message: ChatMessage }\n | { type: \"message.updated\"; message: ChatMessage }\n | { type: \"message.rejected\"; id: string; reason: string }\n | { type: \"message.deleted\"; id: string }\n | { type: \"message.pinned\"; id: string }\n | { type: \"message.unpinned\"; id: string };\n\n/** How a conversation hears about new messages. */\nexport type ChatLiveMode = \"connecting\" | \"live\" | \"polling\" | \"off\";\n","import {\n DEFAULT_CHAT_API_URL,\n DEFAULT_CHAT_ATTACHMENTS_URL,\n type ChatChannelSummary,\n type ChatAttachment,\n type ChatMe,\n type ChatMessage,\n type ChatMessagePage,\n type ChatPack,\n type ChatRealtimeEvent,\n type ChatRealtimeToken,\n type ChatReportReason,\n type ChatSendResult,\n type ChatThreadState,\n type ChatThreadSummary,\n} from \"./types\";\n\n/**\n * An error answer of the chat service (`{\"error\":\"<code>\"}`), or `network`\n * (status 0) when the request did not get an answer.\n *\n * Codes the buyer UI handles: `unauthorized` (401), `plan_required`,\n * `locked`, `muted`, `blocked`, `forbidden`, `read_only` (403), `not_found`\n * (404), `moderation_link`, `moderation_banned_word`, `moderation_media`\n * (422), `cooldown` (429, with `retryAfter`), `unavailable` (503),\n * `invalid_project` (400).\n */\nexport class ChatError extends Error {\n readonly code: string;\n readonly status: number;\n /** `cooldown`: seconds to wait before the next post. */\n readonly retryAfter?: number;\n /** Extra fields of the answer (`locked`: product, price type and bounds). */\n readonly data: Record<string, unknown>;\n\n constructor(\n code: string,\n status: number,\n data: Record<string, unknown> = {},\n ) {\n super(`chat: ${code} (${status})`);\n this.name = \"ChatError\";\n this.code = code;\n this.status = status;\n this.data = data;\n const retry = Number(data.retry_after);\n if (Number.isFinite(retry) && retry > 0) this.retryAfter = retry;\n }\n}\n\nexport type ChatClientOptions = {\n /** Project id sent as `X-Bitsnap-Project`. */\n projectId: string;\n /** Bearer credential: the panel session token or a `bcht_` buyer token. */\n token: string;\n /** Base URL of the chat service. Defaults to `https://chat.bitsnap.pl`. */\n baseUrl?: string;\n /** Bitsnap app host for private attachment upload and read endpoints. */\n attachmentsUrl?: string;\n /** Replaces `fetch` (tests, server rendering). */\n fetch?: typeof fetch;\n};\n\n/** The buyer calls of the chat service HTTP API. */\nexport type ChatClient = {\n readonly baseUrl: string;\n readonly projectId: string;\n me(): Promise<ChatMe>;\n /** Opens or returns the pack thread of a service product. */\n openThread(productId: string): Promise<ChatThreadSummary>;\n /** Newest first; `before` is a message id. */\n messages(\n threadId: string,\n options?: { before?: string; limit?: number },\n ): Promise<ChatMessagePage>;\n /** Queues a message. The service answers 202 with `status: \"pending\"`. */\n send(\n threadId: string,\n input: { body: string; clientId?: string; attachment?: ChatAttachment },\n ): Promise<ChatSendResult>;\n uploadAttachment(threadId: string, file: File, progress: (value: number) => void): Promise<ChatAttachment>;\n attachmentURL(threadId: string, message: ChatMessage): Promise<string>;\n deleteMessage(threadId: string, messageId: string): Promise<void>;\n mute(threadId: string, muted: boolean): Promise<void>;\n report(input: {\n threadId: string;\n messageId?: string;\n reason: ChatReportReason;\n note?: string;\n }): Promise<void>;\n markRead(threadId: string, messageId: string): Promise<void>;\n /**\n * A NATS token for every conversation the caller may read. The answer is\n * kept until a minute and a half before it expires (the service mints one\n * per caller every 10 s); `fresh` asks for a new one.\n */\n realtimeToken(options?: { fresh?: boolean }): Promise<ChatRealtimeToken>;\n};\n\nexport function createChatClient(options: ChatClientOptions): ChatClient {\n const baseUrl = (options.baseUrl || DEFAULT_CHAT_API_URL).replace(/\\/+$/, \"\");\n const localOrigin = typeof window !== \"undefined\" && [\"localhost\", \"127.0.0.1\"].includes(window.location.hostname) ? window.location.origin : undefined;\n const attachmentBase = (options.attachmentsUrl ?? localOrigin ?? DEFAULT_CHAT_ATTACHMENTS_URL).replace(/\\/+$/, \"\");\n const doFetch = options.fetch ?? ((...args) => fetch(...args));\n\n async function call(\n method: string,\n path: string,\n body?: unknown,\n ): Promise<{ status: number; json: unknown }> {\n let response: Response;\n try {\n response = await doFetch(baseUrl + path, {\n method,\n headers: {\n Authorization: `Bearer ${options.token}`,\n \"X-Bitsnap-Project\": options.projectId,\n ...(body === undefined ? {} : { \"Content-Type\": \"application/json\" }),\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n } catch {\n throw new ChatError(\"network\", 0);\n }\n let json: unknown = null;\n try {\n const text = await response.text();\n json = text ? JSON.parse(text) : null;\n } catch {\n json = null;\n }\n if (!response.ok) {\n const data = isRecord(json) ? json : {};\n const code = typeof data.error === \"string\" ? data.error : \"internal_error\";\n throw new ChatError(code, response.status, data);\n }\n return { status: response.status, json };\n }\n\n const id = encodeURIComponent;\n let cachedToken: ChatRealtimeToken | undefined;\n\n return {\n baseUrl,\n projectId: options.projectId,\n async me() {\n const { json } = await call(\"GET\", \"/v1/me\");\n return mapMe(json);\n },\n async openThread(productId) {\n const { json } = await call(\"POST\", \"/v1/threads\", {\n product_id: productId,\n });\n return mapThread(json);\n },\n async messages(threadId, opts = {}) {\n const query = new URLSearchParams();\n if (opts.before) query.set(\"before\", opts.before);\n if (opts.limit) query.set(\"limit\", String(opts.limit));\n const qs = query.toString();\n const suffix = qs ? `?${qs}` : \"\";\n const { json } = await call(\n \"GET\",\n `/v1/threads/${id(threadId)}/messages${suffix}`,\n );\n const page = isRecord(json) ? json : {};\n return {\n messages: arr(page.messages).map(mapMessage),\n hasMore: page.has_more === true,\n };\n },\n async send(threadId, input) {\n const { json } = await call(\n \"POST\",\n `/v1/threads/${id(threadId)}/messages`,\n {\n body: input.body,\n ...(input.clientId ? { client_id: input.clientId } : {}),\n ...(input.attachment ? {\n attachment_key: input.attachment.key,\n attachment_kind: input.attachment.kind,\n attachment_name: input.attachment.filename,\n attachment_size: input.attachment.size,\n attachment_content_type: input.attachment.contentType,\n } : {}),\n },\n );\n const res = isRecord(json) ? json : {};\n return {\n message: mapMessage(res.message),\n thread: mapThreadState(res.thread),\n };\n },\n async uploadAttachment(threadId, file, progress) {\n const requestAttachment = async <T,>(path: string, body: unknown): Promise<T> => {\n const response = await doFetch(`${attachmentBase}/api/chat/attachments/${path}`, {\n method: \"POST\",\n headers: { Authorization: `Bearer ${options.token}`, \"X-Bitsnap-Project\": options.projectId, \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n let data: Record<string, unknown> = {};\n try { data = await response.json() as Record<string, unknown>; } catch { /* parsed as request error below */ }\n if (!response.ok) throw new ChatError(typeof data.error === \"string\" ? data.error : \"request_failed\", response.status, data);\n return data as T;\n };\n const ticket = await requestAttachment<{ key: string; uploadURL: string; uploadTicket: string }>(\"upload\", {\n project_id: options.projectId, thread_id: threadId, filename: file.name, content_type: file.type, size: file.size,\n });\n await new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(\"PUT\", ticket.uploadURL);\n xhr.setRequestHeader(\"Content-Type\", file.type);\n xhr.upload.onprogress = (event) => { if (event.lengthComputable) progress(Math.round(event.loaded / event.total * 100)); };\n xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new ChatError(\"upload_failed\", xhr.status));\n xhr.onerror = () => reject(new ChatError(\"network\", 0));\n xhr.send(file);\n });\n const done = await requestAttachment<{ key: string; kind: \"image\" | \"file\"; filename: string; size: number; content_type: string }>(\"complete\", {\n project_id: options.projectId, thread_id: threadId, key: ticket.key, upload_ticket: ticket.uploadTicket, filename: file.name, content_type: file.type, size: file.size,\n });\n return { key: done.key, kind: done.kind, filename: done.filename, size: done.size, contentType: done.content_type };\n },\n async attachmentURL(threadId, message) {\n if (!message.attachmentKey) throw new ChatError(\"invalid_attachment\", 400);\n const query = new URLSearchParams({ project_id: options.projectId, thread_id: threadId, key: message.attachmentKey, filename: message.attachmentName ?? \"attachment\", size: String(message.attachmentSize ?? 0) });\n const response = await doFetch(`${attachmentBase}/api/chat/attachments/read?${query}`, { headers: { Authorization: `Bearer ${options.token}`, \"X-Bitsnap-Project\": options.projectId } });\n let data: Record<string, unknown> = {};\n try { data = await response.json() as Record<string, unknown>; } catch { /* parsed as request error below */ }\n if (!response.ok || typeof data.url !== \"string\") throw new ChatError(typeof data.error === \"string\" ? data.error : \"request_failed\", response.status, data);\n return data.url;\n },\n async deleteMessage(threadId, messageId) {\n await call(\n \"DELETE\",\n `/v1/threads/${id(threadId)}/messages/${id(messageId)}`,\n );\n },\n async mute(threadId, muted) {\n await call(\"POST\", `/v1/threads/${id(threadId)}/mute`, { muted });\n },\n async report(input) {\n await call(\"POST\", \"/v1/reports\", {\n thread_id: input.threadId,\n ...(input.messageId ? { message_id: input.messageId } : {}),\n reason: input.reason,\n ...(input.note ? { note: input.note } : {}),\n });\n },\n async markRead(threadId, messageId) {\n await call(\"POST\", `/v1/threads/${id(threadId)}/receipts`, {\n state: \"read\",\n message_id: messageId,\n });\n },\n async realtimeToken(opts = {}) {\n if (\n !opts.fresh &&\n cachedToken &&\n Date.parse(cachedToken.expiresAt) - Date.now() > 90_000\n ) {\n return cachedToken;\n }\n const { json } = await call(\"POST\", \"/v1/realtime/token\");\n const res = isRecord(json) ? json : {};\n return (cachedToken = {\n jwt: str(res.jwt),\n expiresAt: str(res.expires_at),\n servers: arr(res.servers).filter((s): s is string => typeof s === \"string\"),\n subjects: arr(res.subjects).filter((s): s is string => typeof s === \"string\"),\n truncated: res.truncated === true,\n });\n },\n };\n}\n\n/** Parses one live event of the thread or channel subject. */\nexport function parseRealtimeEvent(raw: unknown): ChatRealtimeEvent | null {\n if (!isRecord(raw) || typeof raw.type !== \"string\") return null;\n const eventId = str(raw.id);\n switch (raw.type) {\n case \"message.created\":\n return { type: \"message.created\", message: mapMessage(raw) };\n case \"message.updated\":\n return { type: \"message.updated\", message: mapMessage(isRecord(raw.message) ? raw.message : raw) };\n case \"message.rejected\":\n return { type: \"message.rejected\", id: eventId, reason: str(raw.reason) };\n case \"message.deleted\":\n case \"message.pinned\":\n case \"message.unpinned\":\n return { type: raw.type, id: eventId };\n default:\n return null;\n }\n}\n\n// --- wire mapping -------------------------------------------------------\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value != null && !Array.isArray(value);\n}\n\nfunction arr(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction str(value: unknown): string {\n return typeof value === \"string\" ? value : value == null ? \"\" : String(value);\n}\n\nfunction num(value: unknown): number {\n const n = Number(value);\n return Number.isFinite(n) ? n : 0;\n}\n\nfunction optStr(value: unknown): string | undefined {\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n}\n\nfunction time(value: unknown): string | null {\n return typeof value === \"string\" && value !== \"\" ? value : null;\n}\n\nexport function mapMessage(raw: unknown): ChatMessage {\n const m = isRecord(raw) ? raw : {};\n const paymentRequest = m.kind === \"payment_request\" ? mapPaymentRequest(m.payment_request) : undefined;\n return {\n id: str(m.id),\n threadId: str(m.thread_id),\n authorEmail: str(m.author_email),\n body: str(m.body),\n kind: m.kind === \"payment_request\" ? \"payment_request\" : \"text\",\n ...(paymentRequest ? { paymentRequest } : {}),\n ...(m.kind === \"payment_request\" && !paymentRequest ? { paymentRequestInvalid: true } : {}),\n attachmentKey: optStr(m.attachment_key),\n attachmentKind: optStr(m.attachment_kind),\n attachmentName: optStr(m.attachment_name),\n attachmentSize: num(m.attachment_size),\n attachmentContentType: optStr(m.attachment_content_type),\n createdAt: str(m.created_at),\n pinned: m.pinned === true,\n deleted: m.deleted === true,\n system: m.system === true,\n ...(m.status === \"pending\" ? { status: \"pending\" as const } : {}),\n };\n}\n\nfunction mapPaymentRequest(raw: unknown): ChatMessage[\"paymentRequest\"] {\n if (!isRecord(raw)) return undefined;\n const nullableNum = (value: unknown): number | null => value == null ? null : (typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : NaN);\n const minAmountMinor = nullableNum(raw.min_amount_minor);\n const maxAmountMinor = nullableNum(raw.max_amount_minor);\n const amountMinor = raw.amount_minor;\n const extraReplies = raw.extra_replies;\n const extendExpiryDays = raw.extend_expiry_days;\n const validUrl = typeof raw.payment_url === \"string\" && /^https:\\/\\//i.test(raw.payment_url);\n if (\n typeof raw.request_id !== \"string\" || !raw.request_id ||\n typeof raw.title !== \"string\" || (raw.description !== null && typeof raw.description !== \"string\") ||\n (raw.amount_mode !== \"exact\" && raw.amount_mode !== \"pwyw\") ||\n typeof amountMinor !== \"number\" || !Number.isFinite(amountMinor) || amountMinor < 0 ||\n Number.isNaN(minAmountMinor) || Number.isNaN(maxAmountMinor) ||\n (raw.amount_mode === \"pwyw\" && (minAmountMinor == null || maxAmountMinor == null || minAmountMinor > maxAmountMinor)) ||\n typeof raw.currency !== \"string\" || !/^[A-Z]{3}$/.test(raw.currency) ||\n typeof extraReplies !== \"number\" || !Number.isFinite(extraReplies) || extraReplies < 0 ||\n typeof extendExpiryDays !== \"number\" || !Number.isFinite(extendExpiryDays) || extendExpiryDays < 0 ||\n !validUrl || typeof raw.expires_at !== \"string\" || !Number.isFinite(Date.parse(raw.expires_at)) ||\n ![\"pending\", \"paid\", \"expired\", \"cancelled\"].includes(String(raw.status)) ||\n (raw.paid_at != null && typeof raw.paid_at !== \"string\")\n ) return undefined;\n return {\n requestId: raw.request_id,\n title: raw.title,\n description: raw.description ?? \"\",\n amountMode: raw.amount_mode,\n amountMinor,\n minAmountMinor,\n maxAmountMinor,\n currency: raw.currency,\n extraReplies,\n extendExpiryDays,\n paymentUrl: raw.payment_url as string,\n status: raw.status as NonNullable<ChatMessage[\"paymentRequest\"]>[\"status\"],\n expiresAt: raw.expires_at as string,\n paidAt: raw.paid_at as string | null,\n };\n}\n\nfunction mapPack(raw: unknown): ChatPack {\n const p = isRecord(raw) ? raw : {};\n return {\n orderId: str(p.order_id),\n productId: optStr(p.product_id),\n replies: num(p.replies),\n used: num(p.used),\n left: num(p.left),\n expiresAt: time(p.expires_at),\n expired: p.expired === true,\n };\n}\n\nfunction mapThreadState(raw: unknown): ChatThreadState {\n const t = isRecord(raw) ? raw : {};\n return {\n id: str(t.id),\n kind: t.kind === \"channel\" ? \"channel\" : \"pack\",\n repliesTotal: num(t.replies_total),\n repliesUsed: num(t.replies_used),\n repliesLeft: num(t.replies_left),\n expiresAt: time(t.expires_at),\n composerLocked: t.composer_locked === true,\n responseHours: num(t.response_hours),\n };\n}\n\nexport function mapThread(raw: unknown): ChatThreadSummary {\n const t = isRecord(raw) ? raw : {};\n return {\n ...mapThreadState(t),\n channelId: optStr(t.channel_id),\n title: str(t.title),\n lastMessage: isRecord(t.last_message) ? mapMessage(t.last_message) : undefined,\n unread: num(t.unread),\n productId: optStr(t.product_id),\n blocked: t.blocked === true,\n packs: arr(t.packs).map(mapPack),\n muted: t.muted === true,\n };\n}\n\nfunction mapChannel(raw: unknown): ChatChannelSummary {\n const c = isRecord(raw) ? raw : {};\n return {\n id: str(c.id),\n name: str(c.name),\n topic: str(c.topic),\n welcome: str(c.welcome),\n whoCanPost: c.who_can_post === \"members\" ? \"members\" : \"creator\",\n productId: str(c.product_id),\n role: str(c.role),\n memberCount: num(c.member_count),\n createdAt: str(c.created_at),\n blockLinks: c.block_links === true,\n blockMedia: c.block_media === true,\n cooldownSeconds: num(c.cooldown_seconds),\n };\n}\n\nfunction mapMe(raw: unknown): ChatMe {\n const m = isRecord(raw) ? raw : {};\n const ent = isRecord(m.entitlements) ? m.entitlements : {};\n return {\n email: str(m.email),\n threads: arr(m.threads).map(mapThread),\n entitlements: {\n packs: arr(ent.packs).map((raw) => {\n const p = isRecord(raw) ? raw : {};\n return {\n productId: str(p.product_id),\n name: str(p.name),\n repliesLeftHint: num(p.replies_left_hint),\n expiresAt: time(p.expires_at),\n };\n }),\n channels: arr(ent.channels).map((raw) => {\n const c = isRecord(raw) ? raw : {};\n return { productId: str(c.product_id), channelId: optStr(c.channel_id) };\n }),\n },\n channels: arr(m.channels).map(mapChannel),\n };\n}\n"]}
1
+ {"version":3,"sources":["/home/emems/.t3/worktrees/bitsnap-react/emems-order-history-panel/dist/chat.js","../src/chat/widgets.tsx","../src/chat/buy.ts","../src/chat/types.ts","../src/chat/client.ts"],"names":["addPackToCart","input","addProductToCart","DEFAULT_CHAT_API_URL","DEFAULT_CHAT_ATTACHMENTS_URL","DEFAULT_CHAT_REALTIME_URL","ChatError","code","status","data","retry","createChatClient","options","baseUrl","localOrigin","attachmentBase","doFetch","args","call","method","path","body","response"],"mappings":"AAAA,y/BAAwC,sDAAyC,sDAAyC,sDAAiD,+BAA4B,sDAAwC,+BAA4B,+BAA4B,sDAAgD,+BAA4B,8BCA9T,MCqBrD,SAAsBA,EAAAA,CAAcC,CAAAA,CAAoC,CACtE,MAAMC,gCAAAA,CAEF,EAAA,CAAID,CAAAA,CAAM,SAAA,CACV,IAAA,kBAAMA,CAAAA,CAAM,IAAA,SAAQ,IAAA,CACpB,KAAA,kBAAOA,CAAAA,CAAM,UAAA,SAAc,GAAA,CAC3B,QAAA,CAAU,EAAA,CACV,MAAA,CAAQA,CAAAA,CAAM,MAChB,CAAA,CACA,CAAE,SAAA,CAAWA,CAAAA,CAAM,SAAA,CAAW,IAAA,CAAMA,CAAAA,CAAM,IAAA,CAAM,QAAA,CAAU,CAAA,CAAK,CACjE,CACF,CCzBO,IAAME,EAAAA,CAAuB,yBAAA,CAEvBC,EAAAA,wCAA+B,oBAAA,CAG/BC,EAAAA,qCACX,oDAAA,CCcK,IAAMC,CAAAA,CAAN,MAAA,QAAwB,KAAM,CAC1B,WAOT,CACEC,CAAAA,CACAC,CAAAA,CACAC,CAAAA,CAAgC,CAAC,CAAA,CACjC,CACA,KAAA,CAAM,CAAA,MAAA,EAASF,CAAI,CAAA,EAAA,EAAKC,CAAM,CAAA,CAAA,CAAG,CAAA,CACjC,IAAA,CAAK,IAAA,CAAO,WAAA,CACZ,IAAA,CAAK,IAAA,CAAOD,CAAAA,CACZ,IAAA,CAAK,MAAA,CAASC,CAAAA,CACd,IAAA,CAAK,IAAA,CAAOC,CAAAA,CACZ,IAAMC,CAAAA,CAAQ,MAAA,CAAOD,CAAAA,CAAK,WAAW,CAAA,CACjC,MAAA,CAAO,QAAA,CAASC,CAAK,CAAA,EAAKA,CAAAA,CAAQ,CAAA,EAAA,CAAG,IAAA,CAAK,UAAA,CAAaA,CAAAA,CAC7D,CACF,CAAA,CAmDO,SAASC,EAAAA,CAAiBC,CAAAA,CAAwC,CACvE,IAAMC,CAAAA,CAAAA,CAAWD,CAAAA,CAAQ,OAAA,EAAWT,EAAAA,CAAAA,CAAsB,OAAA,CAAQ,MAAA,CAAQ,EAAE,CAAA,CACtEW,CAAAA,CAAc,OAAO,MAAA,CAAW,GAAA,EAAe,CAAC,WAAA,CAAa,WAAW,CAAA,CAAE,QAAA,CAAS,MAAA,CAAO,QAAA,CAAS,QAAQ,CAAA,CAAI,MAAA,CAAO,QAAA,CAAS,MAAA,CAAS,KAAA,CAAA,CACxIC,CAAAA,CAAAA,mCAAkBH,CAAAA,CAAQ,cAAA,SAAkBE,GAAAA,SAAeV,IAAAA,CAAAA,CAA8B,OAAA,CAAQ,MAAA,CAAQ,EAAE,CAAA,CAC3GY,CAAAA,kBAAUJ,CAAAA,CAAQ,KAAA,SAAA,CAAU,CAAA,GAAIK,CAAAA,CAAAA,EAAS,KAAA,CAAM,GAAGA,CAAI,CAAA,GAAA,CAE5D,MAAA,SAAeC,CAAAA,CACbC,CAAAA,CACAC,CAAAA,CACAC,CAAAA,CAC4C,CAC5C,IAAIC,CAAAA,CACJ,GAAI,CACFA,CAAAA,CAAW,MAAMN,CAAAA,CAAQH,CAAAA,CAAUO,CAAAA,CAAM,CACvC,MAAA,CAAAD,CAAAA,CACA,OAAA,CAAS,CACP,aAAA,CAAe,CAAA,OAAA,EAAUP,CAAAA,CAAQ,KAAK,CAAA,CAAA","file":"/home/emems/.t3/worktrees/bitsnap-react/emems-order-history-panel/dist/chat.js","sourcesContent":[null,"import { useEffect, useMemo, useRef, useState } from \"react\";\nimport { getLocale } from \"../components/checkout/locale\";\nimport { getPanelConfig } from \"../panel/config\";\nimport { getPanelSession } from \"../panel/session-storage\";\nimport { addPackToCart } from \"./buy\";\nimport { createChatClient, type ChatClient } from \"./client\";\nimport { ConversationView } from \"./Conversation\";\nimport type { NatsModule } from \"./realtime\";\nimport { getChatTexts, type ChatTexts } from \"./texts\";\nimport { useConversation, type ConversationLock } from \"./useConversation\";\n\n/** What the lock's buy action gets. */\nexport type ChatBuyRequest = {\n /** The product (or variant) to sell: `buyProductId`, else the locked product. */\n productId: string;\n /** `replies`, `expired`, `no-pack` or `no-access`. */\n reason: ConversationLock[\"reason\"];\n /** Known for a locked channel (from the chat service). */\n priceCents?: number;\n amount?: number;\n email?: string;\n code?: string;\n turnstileToken?: string;\n};\n\n/** Props shared by `ChatThread` and `Channel`. */\nexport type ChatWidgetBaseProps = {\n /**\n * Public Bitsnap project id of the shop. The cart uses it, the panel\n * session is looked up with it, and it is the `X-Bitsnap-Project` of the\n * chat service unless `chatProjectID` is given.\n */\n projectID: string;\n /** Project id for the chat service, when it differs from `projectID`. */\n chatProjectID?: string;\n /**\n * Bearer for the chat service: a `bcht_` buyer token from your server\n * (`POST /api/v1/chat/buyer-token`). Defaults to the customer panel session\n * of this browser (after `PanelLogin`).\n */\n token?: string;\n /** Chat service base URL. Defaults to `https://chat.bitsnap.pl`. */\n apiUrl?: string;\n /** Bitsnap app host serving private chat attachment APIs (defaults to the page origin). */\n attachmentsUrl?: string;\n /** NATS WebSocket URL overriding the one of the realtime token. */\n realtimeUrl?: string;\n /** Bitsnap host of the panel session (`setHost`), when not the default. */\n host?: string;\n /** Polling interval in ms while the live connection is down. Default 8000. */\n pollIntervalMs?: number;\n /** Language of the default texts; defaults to the library locale. */\n locale?: string;\n /** Text overrides, whatever the locale. */\n texts?: Partial<ChatTexts>;\n /** Heading of the conversation. */\n title?: string;\n /** Shows a back button in the header. */\n onBack?: () => void;\n /**\n * The lock's \"Kup pakiet\" / \"Kup dostęp\". Default: add the product to the\n * page cart and open it (mount `BitsnapCheckout` on the page). Throw to\n * show the error line.\n */\n onBuy?: (request: ChatBuyRequest) => Promise<void> | void;\n /** The product or variant the buy action sells (e.g. the pack variant). */\n buyProductId?: string;\n className?: string;\n /** A ready client (tests, custom transport). Replaces token and URLs. */\n client?: ChatClient;\n /** @internal Replaces the NATS loader (tests). */\n loadNats?: () => Promise<NatsModule>;\n};\n\nexport type ChatThreadProps = ChatWidgetBaseProps & {\n /** The service product of the pack: opens its thread, creating it if needed. */\n productId?: string;\n /** An existing thread. Give this or `productId`. */\n threadId?: string;\n};\n\nexport type ChannelProps = ChatWidgetBaseProps & {\n /** The channel (its id is also the id of its conversation). */\n channelId: string;\n /** The community product sold by the lock; defaults to the channel's own. */\n productId?: string;\n};\n\n/**\n * The chat client of a widget. The panel session lives in localStorage, so\n * it is read after mounting (`null` until then: the widget stays loading and\n * the server HTML has no session-dependent content).\n */\nfunction useWidgetClient(props: ChatWidgetBaseProps): ChatClient | null | undefined {\n const [stored, setStored] = useState<string | undefined | null>(null);\n const host = props.host ?? getPanelConfig().host;\n useEffect(() => {\n if (props.client || props.token) return;\n setStored(getPanelSession(props.projectID, host));\n }, [props.client, props.token, props.projectID, host]);\n const token = props.token ?? stored;\n const projectId = props.chatProjectID ?? props.projectID;\n return useMemo(() => {\n if (props.client) return props.client;\n if (token === null) return null;\n if (!token) return undefined;\n return createChatClient({ projectId, token, baseUrl: props.apiUrl, attachmentsUrl: props.attachmentsUrl });\n }, [props.client, token, projectId, props.apiUrl, props.attachmentsUrl]);\n}\n\nfunction useBuy(props: ChatWidgetBaseProps) {\n const claim = useRef<{ key: string; token: string; expires: number } | null>(null);\n return async (lock: ConversationLock & { email?: string; code?: string; turnstileToken?: string }) => {\n const productId = props.buyProductId ?? lock.productId;\n if (!productId) return;\n const request: ChatBuyRequest = { productId, reason: lock.reason, priceCents: lock.priceCents, ...(lock.amount != null ? { amount: lock.amount } : {}), ...(lock.email ? { email: lock.email } : {}), ...(lock.code ? { code: lock.code } : {}), ...(lock.turnstileToken != null ? { turnstileToken: lock.turnstileToken } : {}) };\n if (props.onBuy) {\n await props.onBuy(request);\n return;\n }\n const commerceBase = (props.host ?? getPanelConfig().host ?? \"https://bitsnap.pl\").replace(/\\/+$/, \"\");\n const isFreeClaim = lock.freeClaimLimit != null && (lock.priceType === \"free\" || (lock.priceType === \"pay_what_you_want\" && lock.minimumCents === 0 && request.amount === 0));\n if (isFreeClaim && request.email && request.turnstileToken != null) {\n claim.current = null;\n const response = await fetch(`${commerceBase}/api/integrations/${encodeURIComponent(props.projectID)}/public-commerce/free-claim/request-code`, {\n method: \"POST\", headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ email: request.email, turnstileToken: request.turnstileToken }),\n });\n const result = await response.json() as { code?: string };\n if (!response.ok) throw new Error(result.code ?? \"internal\");\n return;\n }\n if (isFreeClaim && request.email && request.code) {\n const key = JSON.stringify([commerceBase, props.projectID, productId, request.email, request.code]);\n if (!claim.current || claim.current.key !== key || claim.current.expires <= Date.now()) {\n claim.current = null;\n const verify = await fetch(`${commerceBase}/api/integrations/${encodeURIComponent(props.projectID)}/public-commerce/free-claim/verify-code`, {\n method: \"POST\", headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ email: request.email, code: request.code }),\n });\n const verified = await verify.json() as { claimToken?: string; code?: string; expiresIn?: number };\n if (!verify.ok || !verified.claimToken) throw new Error(verified.code ?? \"email-verification-required\");\n claim.current = { key, token: verified.claimToken, expires: Date.now() + (verified.expiresIn ?? 600) * 1000 };\n }\n const bought = await fetch(`${commerceBase}/api/integrations/${encodeURIComponent(props.projectID)}/public-commerce/buy`, {\n method: \"POST\", headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ items: [{ id: productId, quantity: 1, amount: 0 }], details: { email: request.email }, claimToken: claim.current.token }),\n });\n const result = await bought.json() as { success?: boolean; url?: string; code?: string };\n if (!bought.ok || !result.url) throw new Error(result.code ?? \"free-order-not-supported\");\n window.location.assign(result.url);\n return;\n }\n await addPackToCart({\n projectID: props.projectID,\n productId,\n name: props.title,\n priceCents: lock.priceCents,\n amount: lock.amount,\n host: props.host,\n });\n };\n}\n\n/**\n * A buyer's pack thread for one service product: history, live updates,\n * sending with pending and refused states, report and mute, and the lock\n * with \"Kup pakiet\" when the replies are used up or the pack expired.\n * Load `bitsnap-react/dist/index.css` once.\n */\nexport function ChatThread(props: ChatThreadProps) {\n const client = useWidgetClient(props);\n const locale = props.locale ?? getLocale();\n const texts = useMemo(() => getChatTexts(props.texts, locale), [props.texts, locale]);\n const conversation = useConversation({\n client,\n target: { kind: \"thread\", threadId: props.threadId, productId: props.productId },\n pollIntervalMs: props.pollIntervalMs,\n realtimeUrl: props.realtimeUrl,\n loadNats: props.loadNats,\n });\n const buy = useBuy(props);\n return (\n <div className=\"bitsnap-react h-full\">\n <ConversationView\n host={props.host}\n conversation={conversation}\n texts={texts}\n locale={locale}\n kind=\"thread\"\n title={props.title}\n onBack={props.onBack}\n onBuy={buy}\n className={props.className}\n />\n </div>\n );\n}\n\n/**\n * A community channel: messages, live updates, posting when the channel\n * allows it (with the channel's moderation refusals shown on the message),\n * report, and the lock with \"Kup dostęp\" for a buyer without a grant.\n * Load `bitsnap-react/dist/index.css` once.\n */\nexport function Channel(props: ChannelProps) {\n const client = useWidgetClient(props);\n const locale = props.locale ?? getLocale();\n const texts = useMemo(() => getChatTexts(props.texts, locale), [props.texts, locale]);\n const conversation = useConversation({\n client,\n target: { kind: \"channel\", channelId: props.channelId, productId: props.productId },\n pollIntervalMs: props.pollIntervalMs,\n realtimeUrl: props.realtimeUrl,\n loadNats: props.loadNats,\n });\n const buy = useBuy(props);\n return (\n <div className=\"bitsnap-react h-full\">\n <ConversationView\n host={props.host}\n conversation={conversation}\n texts={texts}\n locale={locale}\n kind=\"channel\"\n title={props.title}\n onBack={props.onBack}\n onBuy={buy}\n className={props.className}\n />\n </div>\n );\n}\n","import { addProductToCart } from \"../components/products/add-to-cart\";\n\nexport type BuyPackInput = {\n /** Public project id of the shop (the cart's project). */\n projectID: string;\n /** The product (or variant) to add: the pack's service or community product. */\n productId: string;\n name?: string;\n priceCents?: number;\n amount?: number;\n /** Backend origin of the cart, when not the default. */\n host?: string;\n};\n\n/**\n * Default \"Kup pakiet\" / \"Kup dostęp\" action: adds the product to the page\n * cart (`Bitsnap.addProductToCart`) and opens the cart drawer. The page needs\n * a mounted `BitsnapCheckout`. When the page cart belongs to another project\n * or host nothing changes and the promise rejects; pass `onBuy` to the widget\n * to use your own checkout instead.\n */\nexport async function addPackToCart(input: BuyPackInput): Promise<void> {\n await addProductToCart(\n {\n id: input.productId,\n name: input.name ?? \"\",\n price: input.priceCents ?? 0,\n currency: \"\",\n amount: input.amount,\n },\n { projectID: input.projectID, host: input.host, showCart: true },\n );\n}\n","/**\n * Buyer side of the Bitsnap chat service (`bitsnap-messaging`): the shapes\n * the library works with. The wire is snake_case JSON; `client.ts` maps it to\n * these camelCase types. Times are ISO 8601 strings, `null` when absent.\n */\n\n/** Default base URL of the chat service. */\nexport const DEFAULT_CHAT_API_URL = \"https://chat.bitsnap.pl\";\n/** Bitsnap API that authenticates callers before signing private attachment URLs. */\nexport const DEFAULT_CHAT_ATTACHMENTS_URL = \"https://bitsnap.pl\";\n\n/** Default NATS WebSocket URL, used when the realtime token lists no server. */\nexport const DEFAULT_CHAT_REALTIME_URL =\n \"wss://bitsnap-nats-queue-production.up.railway.app\";\n\n/** One paid order that gives a pack thread creator replies. */\nexport type ChatPack = {\n orderId: string;\n productId?: string;\n replies: number;\n used: number;\n left: number;\n expiresAt: string | null;\n expired: boolean;\n};\n\n/** A stored message, or the pending answer of a send. */\nexport type ChatMessage = {\n id: string;\n threadId: string;\n authorEmail: string;\n body: string;\n /** Missing on older servers; unknown wire kinds are rendered as text. */\n kind: \"text\" | \"payment_request\";\n paymentRequest?: ChatPaymentRequest;\n /** The wire kind was payment_request but its snapshot failed validation. */\n paymentRequestInvalid?: boolean;\n attachmentKey?: string;\n attachmentKind?: string;\n attachmentName?: string;\n attachmentSize?: number;\n attachmentContentType?: string;\n createdAt: string;\n pinned: boolean;\n /** Soft deleted: the body is empty. */\n deleted: boolean;\n /** A message of the service, such as the creator's away notice. */\n system: boolean;\n /** `\"pending\"` in the answer of a send: queued, not stored yet. */\n status?: \"pending\";\n};\n\n/** Immutable buyer-visible snapshot attached to a chat payment request. */\nexport type ChatPaymentRequest = {\n requestId: string;\n title: string;\n description: string;\n amountMode: \"exact\" | \"pwyw\";\n amountMinor: number;\n minAmountMinor: number | null;\n maxAmountMinor: number | null;\n currency: string;\n extraReplies: number;\n extendExpiryDays: number;\n paymentUrl: string;\n status: \"pending\" | \"paid\" | \"expired\" | \"cancelled\";\n expiresAt: string;\n paidAt: string | null;\n};\n\nexport type ChatAttachment = {\n key: string;\n kind: \"image\" | \"file\";\n filename: string;\n size: number;\n contentType: string;\n};\n\n/** The state of a conversation for the caller (`ThreadView` on the wire). */\nexport type ChatThreadSummary = {\n id: string;\n kind: \"pack\" | \"channel\";\n channelId?: string;\n /** For a buyer, the creator's e-mail (pack) or the channel name. */\n title: string;\n lastMessage?: ChatMessage;\n unread: number;\n repliesTotal: number;\n repliesUsed: number;\n repliesLeft: number;\n expiresAt: string | null;\n /** The caller cannot write: every pack has expired. */\n composerLocked: boolean;\n /** The creator blocked this buyer from posting. */\n blocked?: boolean;\n productId?: string;\n /** The creator's promise in hours (\"odpowiedź w ciągu 48 h\"), 0 for none. */\n responseHours: number;\n packs: ChatPack[];\n muted: boolean;\n};\n\n/** A channel the caller may read. */\nexport type ChatChannelSummary = {\n id: string;\n name: string;\n topic: string;\n welcome: string;\n whoCanPost: \"creator\" | \"members\";\n productId: string;\n /** `creator`, `member` or `muted`. */\n role: string;\n memberCount: number;\n createdAt: string;\n blockLinks: boolean;\n blockMedia: boolean;\n cooldownSeconds: number;\n};\n\n/** A pack the buyer holds and has not opened a thread for yet. */\nexport type ChatPackEntitlement = {\n productId: string;\n name: string;\n repliesLeftHint: number;\n expiresAt: string | null;\n};\n\n/** A community product the buyer holds; `channelId` once a channel is bound. */\nexport type ChatChannelEntitlement = {\n productId: string;\n channelId?: string;\n};\n\n/** `GET /v1/me`: the buyer's chat in one project. */\nexport type ChatMe = {\n email: string;\n threads: ChatThreadSummary[];\n entitlements: {\n packs: ChatPackEntitlement[];\n channels: ChatChannelEntitlement[];\n };\n channels: ChatChannelSummary[];\n};\n\n/** One page of history, newest first as the service answers it. */\nexport type ChatMessagePage = {\n messages: ChatMessage[];\n hasMore: boolean;\n};\n\n/** The thread part of a send answer. */\nexport type ChatThreadState = Pick<\n ChatThreadSummary,\n | \"id\"\n | \"kind\"\n | \"repliesTotal\"\n | \"repliesUsed\"\n | \"repliesLeft\"\n | \"expiresAt\"\n | \"composerLocked\"\n | \"responseHours\"\n>;\n\n/** `POST /v1/threads/{id}/messages` answers 202 with the queued message. */\nexport type ChatSendResult = {\n message: ChatMessage;\n thread: ChatThreadState;\n};\n\nexport type ChatReportReason = \"spam\" | \"abuse\" | \"illegal\" | \"other\";\n\n/** `POST /v1/realtime/token`. */\nexport type ChatRealtimeToken = {\n jwt: string;\n expiresAt: string;\n servers: string[];\n subjects: string[];\n truncated: boolean;\n};\n\n/** Live events on the thread or channel subject. */\nexport type ChatRealtimeEvent =\n | { type: \"message.created\"; message: ChatMessage }\n | { type: \"message.updated\"; message: ChatMessage }\n | { type: \"message.rejected\"; id: string; reason: string }\n | { type: \"message.deleted\"; id: string }\n | { type: \"message.pinned\"; id: string }\n | { type: \"message.unpinned\"; id: string };\n\n/** How a conversation hears about new messages. */\nexport type ChatLiveMode = \"connecting\" | \"live\" | \"polling\" | \"off\";\n","import {\n DEFAULT_CHAT_API_URL,\n DEFAULT_CHAT_ATTACHMENTS_URL,\n type ChatChannelSummary,\n type ChatAttachment,\n type ChatMe,\n type ChatMessage,\n type ChatMessagePage,\n type ChatPack,\n type ChatRealtimeEvent,\n type ChatRealtimeToken,\n type ChatReportReason,\n type ChatSendResult,\n type ChatThreadState,\n type ChatThreadSummary,\n} from \"./types\";\n\n/**\n * An error answer of the chat service (`{\"error\":\"<code>\"}`), or `network`\n * (status 0) when the request did not get an answer.\n *\n * Codes the buyer UI handles: `unauthorized` (401), `plan_required`,\n * `locked`, `muted`, `blocked`, `forbidden`, `read_only` (403), `not_found`\n * (404), `moderation_link`, `moderation_banned_word`, `moderation_media`\n * (422), `cooldown` (429, with `retryAfter`), `unavailable` (503),\n * `invalid_project` (400).\n */\nexport class ChatError extends Error {\n readonly code: string;\n readonly status: number;\n /** `cooldown`: seconds to wait before the next post. */\n readonly retryAfter?: number;\n /** Extra fields of the answer (`locked`: product, price type and bounds). */\n readonly data: Record<string, unknown>;\n\n constructor(\n code: string,\n status: number,\n data: Record<string, unknown> = {},\n ) {\n super(`chat: ${code} (${status})`);\n this.name = \"ChatError\";\n this.code = code;\n this.status = status;\n this.data = data;\n const retry = Number(data.retry_after);\n if (Number.isFinite(retry) && retry > 0) this.retryAfter = retry;\n }\n}\n\nexport type ChatClientOptions = {\n /** Project id sent as `X-Bitsnap-Project`. */\n projectId: string;\n /** Bearer credential: the panel session token or a `bcht_` buyer token. */\n token: string;\n /** Base URL of the chat service. Defaults to `https://chat.bitsnap.pl`. */\n baseUrl?: string;\n /** Bitsnap app host for private attachment upload and read endpoints. */\n attachmentsUrl?: string;\n /** Replaces `fetch` (tests, server rendering). */\n fetch?: typeof fetch;\n};\n\n/** The buyer calls of the chat service HTTP API. */\nexport type ChatClient = {\n readonly baseUrl: string;\n readonly projectId: string;\n me(): Promise<ChatMe>;\n /** Opens or returns the pack thread of a service product. */\n openThread(productId: string): Promise<ChatThreadSummary>;\n /** Newest first; `before` is a message id. */\n messages(\n threadId: string,\n options?: { before?: string; limit?: number },\n ): Promise<ChatMessagePage>;\n /** Queues a message. The service answers 202 with `status: \"pending\"`. */\n send(\n threadId: string,\n input: { body: string; clientId?: string; attachment?: ChatAttachment },\n ): Promise<ChatSendResult>;\n uploadAttachment(threadId: string, file: File, progress: (value: number) => void): Promise<ChatAttachment>;\n attachmentURL(threadId: string, message: ChatMessage): Promise<string>;\n deleteMessage(threadId: string, messageId: string): Promise<void>;\n mute(threadId: string, muted: boolean): Promise<void>;\n report(input: {\n threadId: string;\n messageId?: string;\n reason: ChatReportReason;\n note?: string;\n }): Promise<void>;\n markRead(threadId: string, messageId: string): Promise<void>;\n /**\n * A NATS token for every conversation the caller may read. The answer is\n * kept until a minute and a half before it expires (the service mints one\n * per caller every 10 s); `fresh` asks for a new one.\n */\n realtimeToken(options?: { fresh?: boolean }): Promise<ChatRealtimeToken>;\n};\n\nexport function createChatClient(options: ChatClientOptions): ChatClient {\n const baseUrl = (options.baseUrl || DEFAULT_CHAT_API_URL).replace(/\\/+$/, \"\");\n const localOrigin = typeof window !== \"undefined\" && [\"localhost\", \"127.0.0.1\"].includes(window.location.hostname) ? window.location.origin : undefined;\n const attachmentBase = (options.attachmentsUrl ?? localOrigin ?? DEFAULT_CHAT_ATTACHMENTS_URL).replace(/\\/+$/, \"\");\n const doFetch = options.fetch ?? ((...args) => fetch(...args));\n\n async function call(\n method: string,\n path: string,\n body?: unknown,\n ): Promise<{ status: number; json: unknown }> {\n let response: Response;\n try {\n response = await doFetch(baseUrl + path, {\n method,\n headers: {\n Authorization: `Bearer ${options.token}`,\n \"X-Bitsnap-Project\": options.projectId,\n ...(body === undefined ? {} : { \"Content-Type\": \"application/json\" }),\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n } catch {\n throw new ChatError(\"network\", 0);\n }\n let json: unknown = null;\n try {\n const text = await response.text();\n json = text ? JSON.parse(text) : null;\n } catch {\n json = null;\n }\n if (!response.ok) {\n const data = isRecord(json) ? json : {};\n const code = typeof data.error === \"string\" ? data.error : \"internal_error\";\n throw new ChatError(code, response.status, data);\n }\n return { status: response.status, json };\n }\n\n const id = encodeURIComponent;\n let cachedToken: ChatRealtimeToken | undefined;\n\n return {\n baseUrl,\n projectId: options.projectId,\n async me() {\n const { json } = await call(\"GET\", \"/v1/me\");\n return mapMe(json);\n },\n async openThread(productId) {\n const { json } = await call(\"POST\", \"/v1/threads\", {\n product_id: productId,\n });\n return mapThread(json);\n },\n async messages(threadId, opts = {}) {\n const query = new URLSearchParams();\n if (opts.before) query.set(\"before\", opts.before);\n if (opts.limit) query.set(\"limit\", String(opts.limit));\n const qs = query.toString();\n const suffix = qs ? `?${qs}` : \"\";\n const { json } = await call(\n \"GET\",\n `/v1/threads/${id(threadId)}/messages${suffix}`,\n );\n const page = isRecord(json) ? json : {};\n return {\n messages: arr(page.messages).map(mapMessage),\n hasMore: page.has_more === true,\n };\n },\n async send(threadId, input) {\n const { json } = await call(\n \"POST\",\n `/v1/threads/${id(threadId)}/messages`,\n {\n body: input.body,\n ...(input.clientId ? { client_id: input.clientId } : {}),\n ...(input.attachment ? {\n attachment_key: input.attachment.key,\n attachment_kind: input.attachment.kind,\n attachment_name: input.attachment.filename,\n attachment_size: input.attachment.size,\n attachment_content_type: input.attachment.contentType,\n } : {}),\n },\n );\n const res = isRecord(json) ? json : {};\n return {\n message: mapMessage(res.message),\n thread: mapThreadState(res.thread),\n };\n },\n async uploadAttachment(threadId, file, progress) {\n const requestAttachment = async <T,>(path: string, body: unknown): Promise<T> => {\n const response = await doFetch(`${attachmentBase}/api/chat/attachments/${path}`, {\n method: \"POST\",\n headers: { Authorization: `Bearer ${options.token}`, \"X-Bitsnap-Project\": options.projectId, \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n let data: Record<string, unknown> = {};\n try { data = await response.json() as Record<string, unknown>; } catch { /* parsed as request error below */ }\n if (!response.ok) throw new ChatError(typeof data.error === \"string\" ? data.error : \"request_failed\", response.status, data);\n return data as T;\n };\n const ticket = await requestAttachment<{ key: string; uploadURL: string; uploadTicket: string }>(\"upload\", {\n project_id: options.projectId, thread_id: threadId, filename: file.name, content_type: file.type, size: file.size,\n });\n await new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(\"PUT\", ticket.uploadURL);\n xhr.setRequestHeader(\"Content-Type\", file.type);\n xhr.upload.onprogress = (event) => { if (event.lengthComputable) progress(Math.round(event.loaded / event.total * 100)); };\n xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new ChatError(\"upload_failed\", xhr.status));\n xhr.onerror = () => reject(new ChatError(\"network\", 0));\n xhr.send(file);\n });\n const done = await requestAttachment<{ key: string; kind: \"image\" | \"file\"; filename: string; size: number; content_type: string }>(\"complete\", {\n project_id: options.projectId, thread_id: threadId, key: ticket.key, upload_ticket: ticket.uploadTicket, filename: file.name, content_type: file.type, size: file.size,\n });\n return { key: done.key, kind: done.kind, filename: done.filename, size: done.size, contentType: done.content_type };\n },\n async attachmentURL(threadId, message) {\n if (!message.attachmentKey) throw new ChatError(\"invalid_attachment\", 400);\n const query = new URLSearchParams({ project_id: options.projectId, thread_id: threadId, key: message.attachmentKey, filename: message.attachmentName ?? \"attachment\", size: String(message.attachmentSize ?? 0) });\n const response = await doFetch(`${attachmentBase}/api/chat/attachments/read?${query}`, { headers: { Authorization: `Bearer ${options.token}`, \"X-Bitsnap-Project\": options.projectId } });\n let data: Record<string, unknown> = {};\n try { data = await response.json() as Record<string, unknown>; } catch { /* parsed as request error below */ }\n if (!response.ok || typeof data.url !== \"string\") throw new ChatError(typeof data.error === \"string\" ? data.error : \"request_failed\", response.status, data);\n return data.url;\n },\n async deleteMessage(threadId, messageId) {\n await call(\n \"DELETE\",\n `/v1/threads/${id(threadId)}/messages/${id(messageId)}`,\n );\n },\n async mute(threadId, muted) {\n await call(\"POST\", `/v1/threads/${id(threadId)}/mute`, { muted });\n },\n async report(input) {\n await call(\"POST\", \"/v1/reports\", {\n thread_id: input.threadId,\n ...(input.messageId ? { message_id: input.messageId } : {}),\n reason: input.reason,\n ...(input.note ? { note: input.note } : {}),\n });\n },\n async markRead(threadId, messageId) {\n await call(\"POST\", `/v1/threads/${id(threadId)}/receipts`, {\n state: \"read\",\n message_id: messageId,\n });\n },\n async realtimeToken(opts = {}) {\n if (\n !opts.fresh &&\n cachedToken &&\n Date.parse(cachedToken.expiresAt) - Date.now() > 90_000\n ) {\n return cachedToken;\n }\n const { json } = await call(\"POST\", \"/v1/realtime/token\");\n const res = isRecord(json) ? json : {};\n return (cachedToken = {\n jwt: str(res.jwt),\n expiresAt: str(res.expires_at),\n servers: arr(res.servers).filter((s): s is string => typeof s === \"string\"),\n subjects: arr(res.subjects).filter((s): s is string => typeof s === \"string\"),\n truncated: res.truncated === true,\n });\n },\n };\n}\n\n/** Parses one live event of the thread or channel subject. */\nexport function parseRealtimeEvent(raw: unknown): ChatRealtimeEvent | null {\n if (!isRecord(raw) || typeof raw.type !== \"string\") return null;\n const eventId = str(raw.id);\n switch (raw.type) {\n case \"message.created\":\n return { type: \"message.created\", message: mapMessage(raw) };\n case \"message.updated\":\n return { type: \"message.updated\", message: mapMessage(isRecord(raw.message) ? raw.message : raw) };\n case \"message.rejected\":\n return { type: \"message.rejected\", id: eventId, reason: str(raw.reason) };\n case \"message.deleted\":\n case \"message.pinned\":\n case \"message.unpinned\":\n return { type: raw.type, id: eventId };\n default:\n return null;\n }\n}\n\n// --- wire mapping -------------------------------------------------------\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value != null && !Array.isArray(value);\n}\n\nfunction arr(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction str(value: unknown): string {\n return typeof value === \"string\" ? value : value == null ? \"\" : String(value);\n}\n\nfunction num(value: unknown): number {\n const n = Number(value);\n return Number.isFinite(n) ? n : 0;\n}\n\nfunction optStr(value: unknown): string | undefined {\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n}\n\nfunction time(value: unknown): string | null {\n return typeof value === \"string\" && value !== \"\" ? value : null;\n}\n\nexport function mapMessage(raw: unknown): ChatMessage {\n const m = isRecord(raw) ? raw : {};\n const paymentRequest = m.kind === \"payment_request\" ? mapPaymentRequest(m.payment_request) : undefined;\n return {\n id: str(m.id),\n threadId: str(m.thread_id),\n authorEmail: str(m.author_email),\n body: str(m.body),\n kind: m.kind === \"payment_request\" ? \"payment_request\" : \"text\",\n ...(paymentRequest ? { paymentRequest } : {}),\n ...(m.kind === \"payment_request\" && !paymentRequest ? { paymentRequestInvalid: true } : {}),\n attachmentKey: optStr(m.attachment_key),\n attachmentKind: optStr(m.attachment_kind),\n attachmentName: optStr(m.attachment_name),\n attachmentSize: num(m.attachment_size),\n attachmentContentType: optStr(m.attachment_content_type),\n createdAt: str(m.created_at),\n pinned: m.pinned === true,\n deleted: m.deleted === true,\n system: m.system === true,\n ...(m.status === \"pending\" ? { status: \"pending\" as const } : {}),\n };\n}\n\nfunction mapPaymentRequest(raw: unknown): ChatMessage[\"paymentRequest\"] {\n if (!isRecord(raw)) return undefined;\n const nullableNum = (value: unknown): number | null => value == null ? null : (typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : NaN);\n const minAmountMinor = nullableNum(raw.min_amount_minor);\n const maxAmountMinor = nullableNum(raw.max_amount_minor);\n const amountMinor = raw.amount_minor;\n const extraReplies = raw.extra_replies;\n const extendExpiryDays = raw.extend_expiry_days;\n const validUrl = typeof raw.payment_url === \"string\" && /^https:\\/\\//i.test(raw.payment_url);\n if (\n typeof raw.request_id !== \"string\" || !raw.request_id ||\n typeof raw.title !== \"string\" || (raw.description !== null && typeof raw.description !== \"string\") ||\n (raw.amount_mode !== \"exact\" && raw.amount_mode !== \"pwyw\") ||\n typeof amountMinor !== \"number\" || !Number.isFinite(amountMinor) || amountMinor < 0 ||\n Number.isNaN(minAmountMinor) || Number.isNaN(maxAmountMinor) ||\n (raw.amount_mode === \"pwyw\" && (minAmountMinor == null || maxAmountMinor == null || minAmountMinor > maxAmountMinor)) ||\n typeof raw.currency !== \"string\" || !/^[A-Z]{3}$/.test(raw.currency) ||\n typeof extraReplies !== \"number\" || !Number.isFinite(extraReplies) || extraReplies < 0 ||\n typeof extendExpiryDays !== \"number\" || !Number.isFinite(extendExpiryDays) || extendExpiryDays < 0 ||\n !validUrl || typeof raw.expires_at !== \"string\" || !Number.isFinite(Date.parse(raw.expires_at)) ||\n ![\"pending\", \"paid\", \"expired\", \"cancelled\"].includes(String(raw.status)) ||\n (raw.paid_at != null && typeof raw.paid_at !== \"string\")\n ) return undefined;\n return {\n requestId: raw.request_id,\n title: raw.title,\n description: raw.description ?? \"\",\n amountMode: raw.amount_mode,\n amountMinor,\n minAmountMinor,\n maxAmountMinor,\n currency: raw.currency,\n extraReplies,\n extendExpiryDays,\n paymentUrl: raw.payment_url as string,\n status: raw.status as NonNullable<ChatMessage[\"paymentRequest\"]>[\"status\"],\n expiresAt: raw.expires_at as string,\n paidAt: raw.paid_at as string | null,\n };\n}\n\nfunction mapPack(raw: unknown): ChatPack {\n const p = isRecord(raw) ? raw : {};\n return {\n orderId: str(p.order_id),\n productId: optStr(p.product_id),\n replies: num(p.replies),\n used: num(p.used),\n left: num(p.left),\n expiresAt: time(p.expires_at),\n expired: p.expired === true,\n };\n}\n\nfunction mapThreadState(raw: unknown): ChatThreadState {\n const t = isRecord(raw) ? raw : {};\n return {\n id: str(t.id),\n kind: t.kind === \"channel\" ? \"channel\" : \"pack\",\n repliesTotal: num(t.replies_total),\n repliesUsed: num(t.replies_used),\n repliesLeft: num(t.replies_left),\n expiresAt: time(t.expires_at),\n composerLocked: t.composer_locked === true,\n responseHours: num(t.response_hours),\n };\n}\n\nexport function mapThread(raw: unknown): ChatThreadSummary {\n const t = isRecord(raw) ? raw : {};\n return {\n ...mapThreadState(t),\n channelId: optStr(t.channel_id),\n title: str(t.title),\n lastMessage: isRecord(t.last_message) ? mapMessage(t.last_message) : undefined,\n unread: num(t.unread),\n productId: optStr(t.product_id),\n blocked: t.blocked === true,\n packs: arr(t.packs).map(mapPack),\n muted: t.muted === true,\n };\n}\n\nfunction mapChannel(raw: unknown): ChatChannelSummary {\n const c = isRecord(raw) ? raw : {};\n return {\n id: str(c.id),\n name: str(c.name),\n topic: str(c.topic),\n welcome: str(c.welcome),\n whoCanPost: c.who_can_post === \"members\" ? \"members\" : \"creator\",\n productId: str(c.product_id),\n role: str(c.role),\n memberCount: num(c.member_count),\n createdAt: str(c.created_at),\n blockLinks: c.block_links === true,\n blockMedia: c.block_media === true,\n cooldownSeconds: num(c.cooldown_seconds),\n };\n}\n\nfunction mapMe(raw: unknown): ChatMe {\n const m = isRecord(raw) ? raw : {};\n const ent = isRecord(m.entitlements) ? m.entitlements : {};\n return {\n email: str(m.email),\n threads: arr(m.threads).map(mapThread),\n entitlements: {\n packs: arr(ent.packs).map((raw) => {\n const p = isRecord(raw) ? raw : {};\n return {\n productId: str(p.product_id),\n name: str(p.name),\n repliesLeftHint: num(p.replies_left_hint),\n expiresAt: time(p.expires_at),\n };\n }),\n channels: arr(ent.channels).map((raw) => {\n const c = isRecord(raw) ? raw : {};\n return { productId: str(c.product_id), channelId: optStr(c.channel_id) };\n }),\n },\n channels: arr(m.channels).map(mapChannel),\n };\n}\n"]}