{"name":"@playlive/twitch-shared","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@playlive/twitch-shared","version":"0.1.0","description":"Zero-dep TypeScript interfaces shared across the @playlive/twitch-* family.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"}},"peerDependencies":{},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-akXf7b6aHSxYjTOBGB/rzxbuLF8DGESBoK7nBR8rmQGfhG1iaCAM4vPOMyhGhLhtKn7SPI6L2vwQ+QnvI0ktHw==","shasum":"151b404ba0634909f09afd9995b8a30f49d97a87","readme":"# @playlive/twitch-shared\n\nZero-dependency TypeScript interfaces shared across the\n`@playlive/twitch-*` family (charity, helix, extension, eventsub) and any\ndownstream consumer (the unified data pipeline backend, the `dev/greenroom`\nsimulator, future tooling).\n\nThis package ships no runtime code — only type declarations and a couple of\nfrozen constants for the audit scripts. Lifted from\n`playlive-unified-data-pipeline/packages/core/twitch-shared` where the same\npackage exists to break a circular dependency between the Twitch webhooks\ndispatcher and the chatbot feature.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add -D @playlive/twitch-shared\n```\n\nNo peer dependencies, no runtime dependencies. The package is `sideEffects:\nfalse` and tree-shakes to zero bytes for consumers that import only types.\n\n## Quick start\n\n```ts\nimport type {\n  ITwitchAPIClient,\n  RawTwitchAppToken,\n  TwitchChannel,\n} from \"@playlive/twitch-shared\";\n\nfunction describeChannel(channel: TwitchChannel): string {\n  return `${channel.broadcaster_name} — ${channel.title}`;\n}\n\nclass MyHelixShim implements ITwitchAPIClient {\n  async getChannelInfo(_id: string): Promise<TwitchChannel | null> {\n    return null;\n  }\n  async getTwitchUserData() {\n    return null;\n  }\n  async sendChatMessage() {\n    return {};\n  }\n}\n```\n\n## Subpath exports\n\n| Subpath                  | Description                            |\n| ------------------------ | -------------------------------------- |\n| `@playlive/twitch-shared` | Single barrel — every type + constant. |\n\nThere's deliberately only one entrypoint: the package is so small that subpath\nsplitting would just add noise to consumers.\n\n## API reference\n\n| Export                  | Kind      | Notes                                            |\n| ----------------------- | --------- | ------------------------------------------------ |\n| `TwitchChannel`         | interface | Helix `GET /helix/channels` shape.               |\n| `TwitchStream`          | interface | Helix `GET /helix/streams` shape.                |\n| `TwitchUser`            | interface | Helix `GET /helix/users` minimal shape.          |\n| `RawTwitchAppToken`     | interface | Client-credentials token response.               |\n| `RawTwitchUserToken`    | interface | Authorisation-code token response.               |\n| `RawTwitchCharity`      | interface | Helix `GET /helix/charity/campaigns` shape.      |\n| `ITwitchAPIClient`      | interface | Minimum Helix-client surface (chatbot-friendly). |\n| `ITwitchChatbotDatabase`| interface | Minimum chatbot-database surface.                |\n| `PACKAGE_NAME`          | const     | `\"@playlive/twitch-shared\"`.                     |\n| `KNOWN_URLS`            | const     | Empty — no network traffic from this package.    |\n\nRun `bun run docs:build` to emit the TypeDoc site at `dist/docs/`.\n\n## Upstream spec\n\nType shapes track the Twitch Helix reference at\n<https://dev.twitch.tv/docs/api/reference/>. The local snapshot lives at\n[`specs/twitch/helix-reference.json`](../../../specs/twitch/helix-reference.json) and\nis re-synced via `bun run scripts/sync-twitch-reference.ts`. See\n[`docs/charity-spec-sync.md`](../../../docs/charity-spec-sync.md) for the full flow.\n\n## Twitch Extension URL disclosure\n\nThis package performs no `fetch` or `WebSocket` traffic, so the\n`KNOWN_URLS` export is empty:\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/twitch-shared\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [`docs/twitch-extension-checklist.md`](../../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nRealistic end-to-end scenarios (channel + charity + EventSub) land in\n`dev/greenroom` (phase 12) once the simulator harness is wired up.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../../CONTRIBUTING.md). To add a new shared shape,\nkeep it framework-free and dependency-free — anything that needs `fetch`,\n`crypto`, or framework code belongs in `@playlive/twitch-helix`,\n`@playlive/twitch-charity`, `@playlive/twitch-extension`, or\n`@playlive/twitch-eventsub`.\n\n## License\n\nMIT — see [LICENSE](../../../LICENSE). Distributed via Play Live CodeArtifact\n(PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/twitch-shared/-/twitch-shared-0.1.0.tgz","shasum":"151b404ba0634909f09afd9995b8a30f49d97a87","integrity":"sha512-akXf7b6aHSxYjTOBGB/rzxbuLF8DGESBoK7nBR8rmQGfhG1iaCAM4vPOMyhGhLhtKn7SPI6L2vwQ+QnvI0ktHw=="}},"0.1.1":{"name":"@playlive/twitch-shared","version":"0.1.1","description":"Zero-dep TypeScript interfaces shared across the @playlive/twitch-* family.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"}},"peerDependencies":{},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-RdGZtE7EamrENbg3woZk6tMtw18Whdt06RcZvLsGcYqLCMTe/71q88uB80A6Osm2pU/2wLP39awKZ/k7XKw30A==","shasum":"08fe3c61442e2c445264de08a8483147f14b97b6","readme":"# @playlive/twitch-shared\n\nZero-dependency TypeScript interfaces shared across the\n`@playlive/twitch-*` family (charity, helix, extension, eventsub) and any\ndownstream consumer (the unified data pipeline backend, the `dev/greenroom`\nsimulator, future tooling).\n\nThe package ships two frozen constants (`PACKAGE_NAME`, `KNOWN_URLS`) and\nnothing else at runtime — every other export is an `interface` that disappears\nat build time. Lifted from\n`playlive-unified-data-pipeline/packages/core/twitch-shared` where the same\npackage exists to break a circular dependency between the Twitch webhooks\ndispatcher and the chatbot feature.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/twitch-shared\n```\n\nNo peer dependencies and no runtime dependencies.\n\nIf you only ever write `import type { … }`, the package erases completely and\ncan live in `devDependencies` instead:\n\n```bash\nbun add -D @playlive/twitch-shared\n```\n\nImporting the `PACKAGE_NAME` / `KNOWN_URLS` values makes it a real runtime\ndependency — use the plain `bun add` form in that case. The package is\n`sideEffects: false`, so a type-only import tree-shakes to zero bytes either\nway.\n\n## Quick start\n\n```ts\nimport type { TwitchChannel } from \"@playlive/twitch-shared\";\n\nfunction describeChannel(channel: TwitchChannel): string {\n  return `${channel.broadcaster_name} — ${channel.title} (${channel.game_name})`;\n}\n```\n\nField names are the upstream Helix JSON verbatim (snake_case) so a raw response\nbody can be cast without renaming:\n\n```ts\nconst body = (await res.json()) as { data: TwitchChannel[] };\nconst channel = body.data[0];\n```\n\n## Subpath exports\n\n| Subpath                   | Description                            |\n| ------------------------- | -------------------------------------- |\n| `@playlive/twitch-shared` | Single barrel — every type + constant. |\n\nThere's deliberately only one entrypoint: the package is so small that subpath\nsplitting would just add noise to consumers.\n\n## API reference\n\n| Export                   | Kind      | Notes                                                                             |\n| ------------------------ | --------- | --------------------------------------------------------------------------------- |\n| `TwitchChannel`          | interface | Helix `GET /helix/channels?broadcaster_id=…` shape.                               |\n| `TwitchStream`           | interface | Helix `GET /helix/streams?user_id=…` shape.                                       |\n| `TwitchUser`             | interface | Helix `GET /helix/users?login=…` minimal shape (`id` / `login` / `display_name`). |\n| `RawTwitchAppToken`      | interface | Client-credentials token response (`refresh_token` optional).                     |\n| `RawTwitchUserToken`     | interface | Authorisation-code token response (`refresh_token` + `scope[]` required).         |\n| `RawTwitchCharity`       | interface | Helix `GET /helix/charity/campaigns` shape (snake_case, minor-unit amounts).      |\n| `ITwitchAPIClient`       | interface | Minimum Helix-client surface (chatbot-friendly, 3 methods).                       |\n| `ITwitchChatbotDatabase` | interface | Minimum chatbot-database surface (2 methods).                                     |\n| `PACKAGE_NAME`           | const     | `\"@playlive/twitch-shared\"`. Runtime value.                                        |\n| `KNOWN_URLS`             | const     | Frozen empty array — no network traffic from this package.                        |\n\nMethod signatures for the two pluggable interfaces:\n\n```ts\ninterface ITwitchAPIClient {\n  getChannelInfo(twitchUserID: string): Promise<TwitchChannel | null>;\n  getTwitchUserData(args: {\n    token?: string;\n    twitchUserID?: string;\n    twitchUsername?: string;\n  }): Promise<TwitchUser | null>;\n  sendChatMessage(\n    message: string,\n    broadcasterUserID: string,\n    prefix?: string,\n  ): Promise<unknown>;\n}\n\ninterface ITwitchChatbotDatabase {\n  saveBotOAuth(token: RawTwitchAppToken): Promise<unknown>;\n  setStreamStatus(twitchUsername: string, online: boolean): Promise<unknown>;\n}\n```\n\nRun `bun run docs:build` to emit the TypeDoc site at `dist/docs/`.\n\n## Upstream spec\n\nType shapes track the Twitch Helix reference at\n<https://dev.twitch.tv/docs/api/reference/>. The local snapshot lives at\n[`specs/twitch/helix-reference.json`](../../../specs/twitch/helix-reference.json) and\nis re-synced via `bun run scripts/sync-twitch-reference.ts`. See\n[`docs/charity-spec-sync.md`](../../../docs/charity-spec-sync.md) for the full flow.\n\n## Twitch Extension URL disclosure\n\nThis package performs no `fetch` or `WebSocket` traffic, so the\n`KNOWN_URLS` export is a frozen empty array:\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/twitch-shared\";\nconsole.log(KNOWN_URLS); // []\nconsole.log(Object.isFrozen(KNOWN_URLS)); // true\n```\n\nSee [`docs/twitch-extension-checklist.md`](../../../docs/twitch-extension-checklist.md).\n\n## Examples\n\n### Implementing `ITwitchAPIClient` over native `fetch`\n\n`ITwitchAPIClient` exists so downstream code (the chatbot feature, GreenRoom\nscenarios, tests) can depend on a *behaviour* instead of on\n[`@playlive/twitch-helix`](../helix/) directly, which keeps build graphs\nacyclic. Any object with these three methods is a valid client:\n\n```ts\nimport type {\n  ITwitchAPIClient,\n  RawTwitchAppToken,\n  TwitchChannel,\n  TwitchUser,\n} from \"@playlive/twitch-shared\";\n\nconst HELIX = \"https://api.twitch.tv/helix\";\n\nexport function createHelixShim(\n  token: RawTwitchAppToken,\n  clientId: string,\n): ITwitchAPIClient {\n  const headers = {\n    Authorization: `Bearer ${token.access_token}`,\n    \"Client-Id\": clientId,\n  };\n\n  return {\n    async getChannelInfo(twitchUserID: string): Promise<TwitchChannel | null> {\n      const res = await fetch(\n        `${HELIX}/channels?broadcaster_id=${encodeURIComponent(twitchUserID)}`,\n        { headers },\n      );\n      if (!res.ok) return null;\n      const body = (await res.json()) as { data: TwitchChannel[] };\n      return body.data[0] ?? null;\n    },\n\n    async getTwitchUserData({ twitchUserID, twitchUsername }): Promise<TwitchUser | null> {\n      const qs = twitchUserID\n        ? `id=${encodeURIComponent(twitchUserID)}`\n        : `login=${encodeURIComponent(twitchUsername ?? \"\")}`;\n      const res = await fetch(`${HELIX}/users?${qs}`, { headers });\n      if (!res.ok) return null;\n      const body = (await res.json()) as { data: TwitchUser[] };\n      return body.data[0] ?? null;\n    },\n\n    async sendChatMessage(message, broadcasterUserID, prefix): Promise<unknown> {\n      const res = await fetch(`${HELIX}/chat/messages`, {\n        method: \"POST\",\n        headers: { ...headers, \"content-type\": \"application/json\" },\n        body: JSON.stringify({\n          broadcaster_id: broadcasterUserID,\n          sender_id: broadcasterUserID,\n          message: prefix ? `${prefix} ${message}` : message,\n        }),\n      });\n      return res.json();\n    },\n  };\n}\n```\n\nConsuming code never names a concrete client:\n\n```ts\nasync function announceChannel(api: ITwitchAPIClient, broadcasterId: string) {\n  const channel = await api.getChannelInfo(broadcasterId);\n  if (!channel) return;\n  await api.sendChatMessage(`Now playing ${channel.game_name}!`, broadcasterId, \"[bot]\");\n}\n```\n\n### Reading a charity campaign amount\n\n`RawTwitchCharity` mirrors the Helix JSON, so amounts arrive as integer minor\nunits plus a `decimal_places` divisor — never as a float:\n\n```ts\nimport type { RawTwitchCharity } from \"@playlive/twitch-shared\";\n\nconst campaign: RawTwitchCharity = {\n  broadcaster_id: \"1\",\n  broadcaster_login: \"playliver\",\n  broadcaster_name: \"Playliver\",\n  charity_description: \"Finding cures. Saving children.\",\n  charity_logo: \"https://cdn.twitch/st-jude.png\",\n  charity_name: \"St. Jude\",\n  charity_website: \"https://stjude.org\",\n  current_amount: { currency: \"USD\", decimal_places: 2, value: 284_750 },\n  target_amount: { currency: \"USD\", decimal_places: 2, value: 500_000 },\n  id: \"tw-c-1\",\n};\n\nconst raised = campaign.current_amount.value / 10 ** campaign.current_amount.decimal_places;\nconsole.log(raised); // 2847.5\n```\n\nTo translate that into the canonical Tiltify shapes the overlays render, use\nthe converters in [`@playlive/twitch-charity`](../charity/).\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../../CONTRIBUTING.md). To add a new shared shape,\nkeep it framework-free and dependency-free — anything that needs `fetch`,\n`crypto`, or framework code belongs in [`@playlive/twitch-helix`](../helix/),\n[`@playlive/twitch-charity`](../charity/),\n[`@playlive/twitch-extension`](../extension/), or\n[`@playlive/twitch-eventsub`](../eventsub/).\n\n## License\n\nMIT — see [LICENSE](../../../LICENSE). Distributed via Play Live CodeArtifact\n(PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/twitch-shared/-/twitch-shared-0.1.1.tgz","shasum":"08fe3c61442e2c445264de08a8483147f14b97b6","integrity":"sha512-RdGZtE7EamrENbg3woZk6tMtw18Whdt06RcZvLsGcYqLCMTe/71q88uB80A6Osm2pU/2wLP39awKZ/k7XKw30A=="}},"0.1.2":{"name":"@playlive/twitch-shared","version":"0.1.2","description":"Zero-dep TypeScript interfaces shared across the @playlive/twitch-* family.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"}},"peerDependencies":{},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-YCY4/x7Fc1UIQIN1FKc9IqNuxlfvQZ2+bawhFP8/03k15u0ab9CnM+EjQ3YEfswgCBtDnFMWhoNcmYoR8AdseQ==","shasum":"e48586fe247cb2e451f6dfb1a1a36eedf1ea45cd","readme":"# @playlive/twitch-shared\n\nZero-dependency TypeScript interfaces shared across the `@playlive/twitch-*`\nfamily — [`@playlive/twitch-charity`](../charity/),\n[`@playlive/twitch-helix`](../helix/),\n[`@playlive/twitch-extension`](../extension/),\n[`@playlive/twitch-eventsub`](../eventsub/) — and any downstream consumer.\n\nThe package ships two frozen constants (`PACKAGE_NAME`, `KNOWN_URLS`) and\nnothing else at runtime — every other export is an `interface` that disappears\nat build time. It exists so that services and clients can agree on Twitch wire\nshapes and on pluggable client/database contracts without depending on each\nother, which keeps build graphs acyclic.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/twitch-shared\n```\n\nNo peer dependencies and no runtime dependencies.\n\nIf you only ever write `import type { … }`, the package erases completely and\ncan live in `devDependencies` instead:\n\n```bash\nbun add -D @playlive/twitch-shared\n```\n\nImporting the `PACKAGE_NAME` / `KNOWN_URLS` values makes it a real runtime\ndependency — use the plain `bun add` form in that case. The package is\n`sideEffects: false`, so a type-only import tree-shakes to zero bytes either\nway.\n\n## Quick start\n\n```ts\nimport type { TwitchChannel } from \"@playlive/twitch-shared\";\n\nfunction describeChannel(channel: TwitchChannel): string {\n  return `${channel.broadcaster_name} — ${channel.title} (${channel.game_name})`;\n}\n```\n\nField names are the upstream Helix JSON verbatim (snake_case) so a raw response\nbody can be cast without renaming:\n\n```ts\nconst body = (await res.json()) as { data: TwitchChannel[] };\nconst channel = body.data[0];\n```\n\n## Subpath exports\n\n| Subpath                   | Description                            |\n| ------------------------- | -------------------------------------- |\n| `@playlive/twitch-shared` | Single barrel — every type + constant. |\n\nThere's deliberately only one entrypoint: the package is so small that subpath\nsplitting would just add noise to consumers.\n\n## API reference\n\nFull generated API documentation:\n<https://packages.playlive.experience.stjude.org/p/@playlive/twitch-shared/docs/>\n\n| Export                   | Kind      | Notes                                                                             |\n| ------------------------ | --------- | --------------------------------------------------------------------------------- |\n| `TwitchChannel`          | interface | Helix `GET /helix/channels?broadcaster_id=…` shape.                               |\n| `TwitchStream`           | interface | Helix `GET /helix/streams?user_id=…` shape.                                       |\n| `TwitchUser`             | interface | Helix `GET /helix/users?login=…` minimal shape (`id` / `login` / `display_name`). |\n| `RawTwitchAppToken`      | interface | Client-credentials token response (`refresh_token` optional).                     |\n| `RawTwitchUserToken`     | interface | Authorisation-code token response (`refresh_token` + `scope[]` required).         |\n| `RawTwitchCharity`       | interface | Helix `GET /helix/charity/campaigns` shape (snake_case, minor-unit amounts).      |\n| `ITwitchAPIClient`       | interface | Minimum Helix-client surface (chatbot-friendly, 3 methods).                       |\n| `ITwitchChatbotDatabase` | interface | Minimum chatbot-database surface (2 methods).                                     |\n| `PACKAGE_NAME`           | const     | `\"@playlive/twitch-shared\"`. Runtime value.                                        |\n| `KNOWN_URLS`             | const     | Frozen empty array — no network traffic from this package.                        |\n\nMethod signatures for the two pluggable interfaces:\n\n```ts\ninterface ITwitchAPIClient {\n  getChannelInfo(twitchUserID: string): Promise<TwitchChannel | null>;\n  getTwitchUserData(args: {\n    token?: string;\n    twitchUserID?: string;\n    twitchUsername?: string;\n  }): Promise<TwitchUser | null>;\n  sendChatMessage(\n    message: string,\n    broadcasterUserID: string,\n    prefix?: string,\n  ): Promise<unknown>;\n}\n\ninterface ITwitchChatbotDatabase {\n  saveBotOAuth(token: RawTwitchAppToken): Promise<unknown>;\n  setStreamStatus(twitchUsername: string, online: boolean): Promise<unknown>;\n}\n```\n\n## Upstream spec\n\nType shapes track the public Twitch Helix API reference —\n<https://dev.twitch.tv/docs/api/reference/> — which is the authority for every\n`snake_case` field name here. Helix is unversioned (additive changes only), so\nthese interfaces describe the current Helix responses for:\n\n| Interface          | Twitch endpoint                                                                     |\n| ------------------ | ----------------------------------------------------------------------------------- |\n| `TwitchChannel`    | `GET /helix/channels` — <https://dev.twitch.tv/docs/api/reference/#get-channel-information> |\n| `TwitchStream`     | `GET /helix/streams` — <https://dev.twitch.tv/docs/api/reference/#get-streams>       |\n| `TwitchUser`       | `GET /helix/users` — <https://dev.twitch.tv/docs/api/reference/#get-users>           |\n| `RawTwitchCharity` | `GET /helix/charity/campaigns` — <https://dev.twitch.tv/docs/api/reference/#get-charity-campaign> |\n\n`RawTwitchAppToken` / `RawTwitchUserToken` mirror the OAuth token responses\ndocumented at\n<https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/> (client\ncredentials and authorisation code grants respectively).\n\nInterfaces are intentionally minimal subsets: only the fields Play Live\nconsumes are declared, so an upstream addition never breaks a build.\n\n## Twitch Extension URL disclosure\n\nThis package performs no `fetch` or `WebSocket` traffic, so the\n`KNOWN_URLS` export is a frozen empty array:\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/twitch-shared\";\nconsole.log(KNOWN_URLS); // []\nconsole.log(Object.isFrozen(KNOWN_URLS)); // true\n```\n\nNothing from this package needs to appear on a Twitch Extension's manifest URL\nallowlist.\n\n## Examples\n\n### Implementing `ITwitchAPIClient` over native `fetch`\n\n`ITwitchAPIClient` exists so downstream code (chatbots, simulators, tests) can\ndepend on a *behaviour* instead of on\n[`@playlive/twitch-helix`](../helix/) directly, which keeps build graphs\nacyclic. Any object with these three methods is a valid client:\n\n```ts\nimport type {\n  ITwitchAPIClient,\n  RawTwitchAppToken,\n  TwitchChannel,\n  TwitchUser,\n} from \"@playlive/twitch-shared\";\n\nconst HELIX = \"https://api.twitch.tv/helix\";\n\nexport function createHelixShim(\n  token: RawTwitchAppToken,\n  clientId: string,\n): ITwitchAPIClient {\n  const headers = {\n    Authorization: `Bearer ${token.access_token}`,\n    \"Client-Id\": clientId,\n  };\n\n  return {\n    async getChannelInfo(twitchUserID: string): Promise<TwitchChannel | null> {\n      const res = await fetch(\n        `${HELIX}/channels?broadcaster_id=${encodeURIComponent(twitchUserID)}`,\n        { headers },\n      );\n      if (!res.ok) return null;\n      const body = (await res.json()) as { data: TwitchChannel[] };\n      return body.data[0] ?? null;\n    },\n\n    async getTwitchUserData({ twitchUserID, twitchUsername }): Promise<TwitchUser | null> {\n      const qs = twitchUserID\n        ? `id=${encodeURIComponent(twitchUserID)}`\n        : `login=${encodeURIComponent(twitchUsername ?? \"\")}`;\n      const res = await fetch(`${HELIX}/users?${qs}`, { headers });\n      if (!res.ok) return null;\n      const body = (await res.json()) as { data: TwitchUser[] };\n      return body.data[0] ?? null;\n    },\n\n    async sendChatMessage(message, broadcasterUserID, prefix): Promise<unknown> {\n      const res = await fetch(`${HELIX}/chat/messages`, {\n        method: \"POST\",\n        headers: { ...headers, \"content-type\": \"application/json\" },\n        body: JSON.stringify({\n          broadcaster_id: broadcasterUserID,\n          sender_id: broadcasterUserID,\n          message: prefix ? `${prefix} ${message}` : message,\n        }),\n      });\n      return res.json();\n    },\n  };\n}\n```\n\nConsuming code never names a concrete client:\n\n```ts\nasync function announceChannel(api: ITwitchAPIClient, broadcasterId: string) {\n  const channel = await api.getChannelInfo(broadcasterId);\n  if (!channel) return;\n  await api.sendChatMessage(`Now playing ${channel.game_name}!`, broadcasterId, \"[bot]\");\n}\n```\n\n### Reading a charity campaign amount\n\n`RawTwitchCharity` mirrors the Helix JSON, so amounts arrive as integer minor\nunits plus a `decimal_places` divisor — never as a float:\n\n```ts\nimport type { RawTwitchCharity } from \"@playlive/twitch-shared\";\n\nconst campaign: RawTwitchCharity = {\n  broadcaster_id: \"1\",\n  broadcaster_login: \"playliver\",\n  broadcaster_name: \"Playliver\",\n  charity_description: \"Finding cures. Saving children.\",\n  charity_logo: \"https://cdn.twitch/st-jude.png\",\n  charity_name: \"St. Jude\",\n  charity_website: \"https://stjude.org\",\n  current_amount: { currency: \"USD\", decimal_places: 2, value: 284_750 },\n  target_amount: { currency: \"USD\", decimal_places: 2, value: 500_000 },\n  id: \"tw-c-1\",\n};\n\nconst raised = campaign.current_amount.value / 10 ** campaign.current_amount.decimal_places;\nconsole.log(raised); // 2847.5\n```\n\nTo translate that into the canonical Tiltify shapes the overlays render, use\nthe converters in [`@playlive/twitch-charity`](../charity/).\n\n## License\n\nMIT © St. Jude Children's Research Hospital\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/twitch-shared/-/twitch-shared-0.1.2.tgz","shasum":"e48586fe247cb2e451f6dfb1a1a36eedf1ea45cd","integrity":"sha512-YCY4/x7Fc1UIQIN1FKc9IqNuxlfvQZ2+bawhFP8/03k15u0ab9CnM+EjQ3YEfswgCBtDnFMWhoNcmYoR8AdseQ=="}}},"time":{"0.1.0":"2026-08-26T18:10:15.376Z","modified":"2026-08-26T20:06:32.487Z","0.1.1":"2026-08-26T19:42:17.764Z","0.1.2":"2026-08-26T20:06:32.487Z"}}