← all packages

@playlive/tiltify-core

v0.4.19

npm install @playlive/tiltify-core

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


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

Install

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.

Quick start

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 exports

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.

API reference

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.

Upstream spec

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.

Twitch Extension URL disclosure

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.

Migration from @playlive/tiltify-lib

@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().

Examples

Build an overlay payload for one campaign

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

Attribute campaigns to St. Jude

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`);

Reuse the retry policy outside the client

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

License

MIT © St. Jude Children's Research Hospital