← all packages

@playlive/twitch-shared

v0.1.2

npm install @playlive/twitch-shared

Requires @playlive:registry=https://packages.playlive.experience.stjude.org in your .npmrc. packument · tarball · API docs


Zero-dependency TypeScript interfaces shared across the @playlive/twitch-* family — @playlive/twitch-charity, @playlive/twitch-helix, @playlive/twitch-extension, @playlive/twitch-eventsub — and any downstream consumer.

The package ships two frozen constants (PACKAGE_NAME, KNOWN_URLS) and nothing else at runtime — every other export is an interface that disappears at build time. It exists so that services and clients can agree on Twitch wire shapes and on pluggable client/database contracts without depending on each other, which keeps build graphs acyclic.

Coverage

Install

bun add @playlive/twitch-shared

No peer dependencies and no runtime dependencies.

If you only ever write import type { … }, the package erases completely and can live in devDependencies instead:

bun add -D @playlive/twitch-shared

Importing the PACKAGE_NAME / KNOWN_URLS values makes it a real runtime dependency — use the plain bun add form in that case. The package is sideEffects: false, so a type-only import tree-shakes to zero bytes either way.

Quick start

import type { TwitchChannel } from "@playlive/twitch-shared";

function describeChannel(channel: TwitchChannel): string {
  return `${channel.broadcaster_name} — ${channel.title} (${channel.game_name})`;
}

Field names are the upstream Helix JSON verbatim (snake_case) so a raw response body can be cast without renaming:

const body = (await res.json()) as { data: TwitchChannel[] };
const channel = body.data[0];

Subpath exports

Subpath Description
@playlive/twitch-shared Single barrel — every type + constant.

There's deliberately only one entrypoint: the package is so small that subpath splitting would just add noise to consumers.

API reference

Full generated API documentation: https://packages.playlive.experience.stjude.org/p/@playlive/twitch-shared/docs/

Export Kind Notes
TwitchChannel interface Helix GET /helix/channels?broadcaster_id=… shape.
TwitchStream interface Helix GET /helix/streams?user_id=… shape.
TwitchUser interface Helix GET /helix/users?login=… minimal shape (id / login / display_name).
RawTwitchAppToken interface Client-credentials token response (refresh_token optional).
RawTwitchUserToken interface Authorisation-code token response (refresh_token + scope[] required).
RawTwitchCharity interface Helix GET /helix/charity/campaigns shape (snake_case, minor-unit amounts).
ITwitchAPIClient interface Minimum Helix-client surface (chatbot-friendly, 3 methods).
ITwitchChatbotDatabase interface Minimum chatbot-database surface (2 methods).
PACKAGE_NAME const "@playlive/twitch-shared". Runtime value.
KNOWN_URLS const Frozen empty array — no network traffic from this package.

Method signatures for the two pluggable interfaces:

interface ITwitchAPIClient {
  getChannelInfo(twitchUserID: string): Promise<TwitchChannel | null>;
  getTwitchUserData(args: {
    token?: string;
    twitchUserID?: string;
    twitchUsername?: string;
  }): Promise<TwitchUser | null>;
  sendChatMessage(
    message: string,
    broadcasterUserID: string,
    prefix?: string,
  ): Promise<unknown>;
}

interface ITwitchChatbotDatabase {
  saveBotOAuth(token: RawTwitchAppToken): Promise<unknown>;
  setStreamStatus(twitchUsername: string, online: boolean): Promise<unknown>;
}

Upstream spec

Type shapes track the public Twitch Helix API reference — https://dev.twitch.tv/docs/api/reference/ — which is the authority for every snake_case field name here. Helix is unversioned (additive changes only), so these interfaces describe the current Helix responses for:

Interface Twitch endpoint
TwitchChannel GET /helix/channelshttps://dev.twitch.tv/docs/api/reference/#get-channel-information
TwitchStream GET /helix/streamshttps://dev.twitch.tv/docs/api/reference/#get-streams
TwitchUser GET /helix/usershttps://dev.twitch.tv/docs/api/reference/#get-users
RawTwitchCharity GET /helix/charity/campaignshttps://dev.twitch.tv/docs/api/reference/#get-charity-campaign

RawTwitchAppToken / RawTwitchUserToken mirror the OAuth token responses documented at https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/ (client credentials and authorisation code grants respectively).

Interfaces are intentionally minimal subsets: only the fields Play Live consumes are declared, so an upstream addition never breaks a build.

Twitch Extension URL disclosure

This package performs no fetch or WebSocket traffic, so the KNOWN_URLS export is a frozen empty array:

import { KNOWN_URLS } from "@playlive/twitch-shared";
console.log(KNOWN_URLS); // []
console.log(Object.isFrozen(KNOWN_URLS)); // true

Nothing from this package needs to appear on a Twitch Extension's manifest URL allowlist.

Examples

Implementing ITwitchAPIClient over native fetch

ITwitchAPIClient exists so downstream code (chatbots, simulators, tests) can depend on a behaviour instead of on @playlive/twitch-helix directly, which keeps build graphs acyclic. Any object with these three methods is a valid client:

import type {
  ITwitchAPIClient,
  RawTwitchAppToken,
  TwitchChannel,
  TwitchUser,
} from "@playlive/twitch-shared";

const HELIX = "https://api.twitch.tv/helix";

export function createHelixShim(
  token: RawTwitchAppToken,
  clientId: string,
): ITwitchAPIClient {
  const headers = {
    Authorization: `Bearer ${token.access_token}`,
    "Client-Id": clientId,
  };

  return {
    async getChannelInfo(twitchUserID: string): Promise<TwitchChannel | null> {
      const res = await fetch(
        `${HELIX}/channels?broadcaster_id=${encodeURIComponent(twitchUserID)}`,
        { headers },
      );
      if (!res.ok) return null;
      const body = (await res.json()) as { data: TwitchChannel[] };
      return body.data[0] ?? null;
    },

    async getTwitchUserData({ twitchUserID, twitchUsername }): Promise<TwitchUser | null> {
      const qs = twitchUserID
        ? `id=${encodeURIComponent(twitchUserID)}`
        : `login=${encodeURIComponent(twitchUsername ?? "")}`;
      const res = await fetch(`${HELIX}/users?${qs}`, { headers });
      if (!res.ok) return null;
      const body = (await res.json()) as { data: TwitchUser[] };
      return body.data[0] ?? null;
    },

    async sendChatMessage(message, broadcasterUserID, prefix): Promise<unknown> {
      const res = await fetch(`${HELIX}/chat/messages`, {
        method: "POST",
        headers: { ...headers, "content-type": "application/json" },
        body: JSON.stringify({
          broadcaster_id: broadcasterUserID,
          sender_id: broadcasterUserID,
          message: prefix ? `${prefix} ${message}` : message,
        }),
      });
      return res.json();
    },
  };
}

Consuming code never names a concrete client:

async function announceChannel(api: ITwitchAPIClient, broadcasterId: string) {
  const channel = await api.getChannelInfo(broadcasterId);
  if (!channel) return;
  await api.sendChatMessage(`Now playing ${channel.game_name}!`, broadcasterId, "[bot]");
}

Reading a charity campaign amount

RawTwitchCharity mirrors the Helix JSON, so amounts arrive as integer minor units plus a decimal_places divisor — never as a float:

import type { RawTwitchCharity } from "@playlive/twitch-shared";

const campaign: RawTwitchCharity = {
  broadcaster_id: "1",
  broadcaster_login: "playliver",
  broadcaster_name: "Playliver",
  charity_description: "Finding cures. Saving children.",
  charity_logo: "https://cdn.twitch/st-jude.png",
  charity_name: "St. Jude",
  charity_website: "https://stjude.org",
  current_amount: { currency: "USD", decimal_places: 2, value: 284_750 },
  target_amount: { currency: "USD", decimal_places: 2, value: 500_000 },
  id: "tw-c-1",
};

const raised = campaign.current_amount.value / 10 ** campaign.current_amount.decimal_places;
console.log(raised); // 2847.5

To translate that into the canonical Tiltify shapes the overlays render, use the converters in @playlive/twitch-charity.

License

MIT © St. Jude Children's Research Hospital