@playlive/twitch-shared
    Preparing search index...

    @playlive/twitch-shared

    @playlive/twitch-shared

    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

    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.

    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 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.

    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>;
    }

    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.

    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.

    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]");
    }

    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.

    MIT © St. Jude Children's Research Hospital