@playlive/tiltify-core
    Preparing search index...

    @playlive/tiltify-core

    @playlive/tiltify-core

    Tiltify v5 REST client (public + private + cause) with native fetch. Singleton ported from @playlive/tiltify-lib; the public surface is preserved verbatim so consumers can migrate by changing the import specifier alone.

    Coverage

    bun add @playlive/tiltify-core
    

    No peer dependencies and no runtime dependencies — peerDependencies in package.json is empty and the client uses the platform's fetch.

    import { tiltify } from "@playlive/tiltify-core";

    // Server-side, client-credential OAuth (reads TILTIFY_CLIENT_ID / TILTIFY_SECRET):
    tiltify.UseClientAuth();
    const events = await tiltify.GetCurrentEvents();

    // Frontend / browser usage via the St. Jude proxy (no credentials shipped):
    tiltify.UseProxy("prod");
    tiltify.SetPublicMode();
    const campaign = await tiltify.FindCampaign("philckd", "philckd-x-st-jude-play-live-2023");

    // Auto-fallback prod → qa → dev on transient errors:
    tiltify.UseProxyWithFallback();

    // User OAuth — caller performs the code-exchange and pushes the token in:
    tiltify.UseUserAuth();
    tiltify.SetCredentials(clientID, clientSecret);
    tiltify.OverrideAuthToken(`Bearer ${accessToken}`, Date.now() + expiresInMs, refreshToken);
    const me = await tiltify.GetCurrentUser();
    Subpath Description
    @playlive/tiltify-core Default barrel — re-exports every module below.
    @playlive/tiltify-core/client Tiltify class + tiltify singleton.
    @playlive/tiltify-core/types Type-only barrel (campaign / donation / user / cause / auction / webhook / common).
    @playlive/tiltify-core/constants URL, header, and version constants (BASE_URL, proxy URLs, SJ_CAUSE_ID, …).
    @playlive/tiltify-core/errors TiltifyLibError, TiltifyErrorCode, TiltifyLibErrorOptions, normalizeEnvelopeError.
    @playlive/tiltify-core/backoff Pure Retry-After / exponential-backoff helpers. No REST deps.
    @playlive/tiltify-core/urls Pure tiltify.com URL parse/build helpers. No REST deps.
    @playlive/tiltify-core/cause-classifier isCampaignForCause / filterCampaignsByCause (performs REST lookups).

    /backoff and /urls are dependency-free — import them when you want the helpers without pulling the client (and its globalThis.fetch touch) into a bundle. auth, logging, and proxy have no dedicated subpath; import those symbols from the default barrel.

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

    Top-level exports:

    Export Source Kind Notes
    Tiltify ./client class Prefer the singleton. Tiltify.Instance is globalThis-pinned.
    tiltify ./client const The singleton (Tiltify.Instance).
    TILTIFY_AUTH_MODE ./auth enum DISABLED / CLIENT / USER.
    TILTIFY_AUTH_STATUS ./auth enum MISSING / VALID / NEEDS_REFRESH.
    TiltifyOAuthScope ./auth type "public" / "cause" / "webhooks:write".
    TiltifyLibError ./errors class Throwable; branch via err.code. .fromTiltifyError(), .toJSON().
    TiltifyErrorCode ./errors type Union of error categories ("NOT_FOUND", "TIMEOUT", …).
    TiltifyLibErrorOptions ./errors type Structured throw-site context.
    normalizeEnvelopeError ./errors function String / object / "HTTP 4xx" body normalizer.
    LogLevel, LogFn ./logging type Pluggable logger primitives.
    LOG_LEVEL_RANK, defaultLogger ./logging const Level ranks + the built-in console.* sink.
    TiltifyProxyEnv ./proxy type "dev" / "qa" / "prod".
    PROXY_URLS, PROXY_FALLBACK_ORDER ./proxy const Env → URL map; ordered fallback envs.
    isProxyEnv, joinURL, shouldFallbackOnError ./proxy function Type guard, slash-safe URL join, transient-error predicate.
    BASE_URL, OAUTH_TOKEN_URL ./constants const Tiltify v5 API + token endpoints.
    PROD_PROXY_URL, QA_PROXY_URL, DEV_PROXY_URL, PROXY_FALLBACK_CHAIN ./constants const Hosted St. Jude proxy URLs + their ordered chain.
    SJ_CAUSE_ID, ALWAYS_INCLUDE_SJ_FUNDRAISING_EVENT_IDS ./constants const St. Jude cause UUID + legacy event-ID allow-set.
    SOURCE_HEADER_NAME, SOURCE_HEADER_VALUE ./constants const Proxy request-tagging header (overridable via SetSourceTag).
    CLIENT_VERSION, CLIENT_VERSION_HEADER_NAME, CLIENT_VERSION_HEADER_VALUE ./constants const Build stamp; proxy-only telemetry header. "dev" when run from source.
    USER_AGENT_HEADER_NAME, USER_AGENT_HEADER_VALUE ./constants const Default outbound UA (overridable via SetUserAgent).
    parseRetryAfter, computeBackoffMs, nextRetrySleepMs ./backoff function RFC 7231 Retry-After parsing + jittered exponential backoff.
    ComputeBackoffOptions ./backoff type Tunables for the two backoff helpers.
    DEFAULT_MAX_BACKOFF_MS, DEFAULT_BASE_BACKOFF_MS, DEFAULT_JITTER_MS ./backoff const 30_000 / 500 / 250.
    normalizeTiltifyUrl, extractSlugsFromTiltifyUrl, isTeamCampaignUrl, buildTiltifyUrl ./urls function Absolute + relative @user / +team URL handling.
    ExtractedSlugs ./urls type { fundraiserType, fundraiserSlug, campaignSlug }.
    isCampaignForCause, filterCampaignsByCause ./cause-classifier function Direct + fundraising-event-mediated cause attribution.
    FundraisingEventLookup, CauseClassifierOptions ./cause-classifier type Injection seam + options bag for the classifier.
    KNOCK_KNOCK_JOKES, getRandomJoke ./knock-knock-jokes const/fn Appended to the outbound User-Agent. Yes, really.
    Tiltify* types ./types type Every public Tiltify resource shape.
    KNOWN_URLS ./ const Twitch Extension URL disclosure list.
    PACKAGE_NAME ./ const Identifier for runtime version-pinning.

    This package targets the Tiltify v5 REST API (https://v5api.tiltify.com) across all three published scopes — public, private, and cause. Upstream reference documentation, OAuth scope descriptions, and per-endpoint payload shapes live at https://developers.tiltify.com.

    Public methods on Tiltify that map 1:1 to an upstream operation carry an @see tag naming the upstream operationId, so you can jump from a method in the generated API docs straight to the matching entry in Tiltify's reference.

    The KNOWN_URLS export enumerates every absolute URL or host this package can fetch — the list a Twitch Extension submission must disclose verbatim.

    import { KNOWN_URLS } from "@playlive/tiltify-core";
    console.log(KNOWN_URLS);
    // [
    // "https://v5api.tiltify.com",
    // "https://tiltify-proxy.prod.experience.stjude.org",
    // "https://tiltify-proxy.qa.experience.stjude.org",
    // "https://tiltify-proxy.dev.experience.stjude.org",
    // ]

    Keep this list and the source export in sync — the Extension submission form requires the disclosure list verbatim.

    @playlive/tiltify-core is a drop-in replacement: the Tiltify class, the tiltify singleton, every method signature, every exported type, every constant, and every TiltifyLibError code are preserved unchanged. Only the import paths move:

    - import { tiltify, TiltifyLibError, BASE_URL } from "@playlive/tiltify-lib";
    + import { tiltify, TiltifyLibError, BASE_URL } from "@playlive/tiltify-core";
    

    The build is ESM-only (the legacy package shipped dual ESM+CJS); CommonJS consumers need a dynamic import().

    Configure the singleton once at boot, resolve a tiltify.com URL to its slugs, then fan out to the per-campaign resources. Every method throws TiltifyLibError, so a single catch covers the whole batch — branch on err.code rather than parsing messages.

    import {
    extractSlugsFromTiltifyUrl,
    TiltifyLibError,
    tiltify,
    type TiltifyCampaign,
    type TiltifyDonation,
    type TiltifyLeaderboardEntry,
    type TiltifyMilestone,
    type TiltifyTeamCampaign,
    } from "@playlive/tiltify-core";

    // ── Boot-time configuration (do this once) ────────────────────────────────
    tiltify.UseProxyWithFallback("playlive-overlay"); // prod → qa → dev, tagged
    tiltify.SetPublicMode();
    tiltify.SetLogLevel("warn");
    tiltify.SetTiltifyTimeout(8_000);
    tiltify.SetMaxRetryDuration(20_000); // cap total retry wall-time

    export interface OverlayPayload {
    campaign: TiltifyCampaign | TiltifyTeamCampaign;
    nextMilestone: TiltifyMilestone | null;
    topDonors: TiltifyLeaderboardEntry[];
    recentDonations: TiltifyDonation[];
    }

    export async function loadOverlay(campaignUrl: string): Promise<OverlayPayload | null> {
    const slugs = extractSlugsFromTiltifyUrl(campaignUrl);
    if (!slugs) {
    throw new TiltifyLibError(`Not a Tiltify campaign URL: ${campaignUrl}`, {
    code: "INVALID_ARGUMENT",
    fields: ["campaignUrl"],
    });
    }

    const isTeam = slugs.fundraiserType === "team";

    try {
    // FindCampaign returns null on 404 rather than throwing.
    const campaign = await tiltify.FindCampaign(
    slugs.fundraiserSlug,
    slugs.campaignSlug,
    isTeam,
    );
    if (!campaign) return null;

    const [milestones, topDonors, recentDonations] = await Promise.all([
    tiltify.GetMilestones(campaign.id, isTeam),
    tiltify.GetLeaderboard(campaign.id, "all", isTeam, 10),
    tiltify.GetCampaignDonations(campaign.id, isTeam, 25),
    ]);

    const raised = Number(campaign.amount_raised?.value ?? 0);
    const nextMilestone =
    milestones
    .filter((m) => m.active && Number(m.amount.value) > raised)
    .sort((a, b) => Number(a.amount.value) - Number(b.amount.value))[0] ?? null;

    return { campaign, nextMilestone, topDonors, recentDonations };
    } catch (err) {
    if (err instanceof TiltifyLibError) {
    // Structured context: code / status / endpoint / cursor / fields.
    if (err.code === "NOT_FOUND") return null;
    if (err.code === "TIMEOUT" || (err.status ?? 0) >= 500) {
    console.warn("tiltify upstream degraded", err.toJSON());
    return null;
    }
    }
    throw err;
    }
    }

    A campaign belongs to a cause either directly (cause_id matches) or indirectly (its fundraising event's cause_id matches). filterCampaignsByCause resolves both, memoizing each unique fundraising_event_id so a 500-campaign list issues one lookup per event.

    import {
    ALWAYS_INCLUDE_SJ_FUNDRAISING_EVENT_IDS,
    filterCampaignsByCause,
    SJ_CAUSE_ID,
    tiltify,
    } from "@playlive/tiltify-core";

    tiltify.UseClientAuth();
    tiltify.SetCredentials(process.env.TILTIFY_CLIENT_ID!, process.env.TILTIFY_SECRET!);

    const { data: events } = await tiltify.GetCurrentEvents();
    const supporting = await tiltify.GetFundraisingEventsSupportingCampaigns(events[0]!.id, 100);

    const stJudeOnly = await filterCampaignsByCause(supporting, SJ_CAUSE_ID, {
    // Legacy Play Live events whose upstream cause attribution drifted.
    alwaysIncludeEventIds: ALWAYS_INCLUDE_SJ_FUNDRAISING_EVENT_IDS,
    });

    console.log(`${stJudeOnly.length}/${supporting.length} campaigns are for St. Jude`);

    @playlive/tiltify-core/backoff is pure and dependency-free — the same policy PerformRequest uses internally.

    import { nextRetrySleepMs } from "@playlive/tiltify-core/backoff";

    // Server said "Retry-After: 2" → 2000ms, capped at DEFAULT_MAX_BACKOFF_MS.
    nextRetrySleepMs("2", 0, Date.now()); // 2000

    // No header → exponential + jitter for the 3rd retry (attempt is 0-based).
    nextRetrySleepMs(null, 2, Date.now(), { baseMs: 500, jitterMs: 0 }); // 2000

    Realtime traffic for the same campaign is available over Tiltify's Phoenix Channels gateway via @playlive/tiltify-phoenix, which uses this client for its slug → fact-id lookups.

    MIT © St. Jude Children's Research Hospital