@ufcalendar/sdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,6 +49,8 @@ List endpoints are **async generators** that follow `meta.pagination.next_cursor
49
49
  | `orgs()` / `org(slug)` | `GET /v1/orgs` / `…/{slug}` |
50
50
  | `events({ org, status, from, to, order, limit })` | `GET /v1/events` (paginated) |
51
51
  | `event(slug)` / `eventChanges(slug)` | `GET /v1/events/{slug}` / `…/changes` |
52
+ | `eventLive(slug)` | `GET /v1/events/{slug}/live` — real-time `LiveState` on fight night (Pro plans and up); the same document streams over `wss://live.ufcalendar.com/v1?key=…` |
53
+ | `subscribeLive(slug, onFrame, opts?)` | the **WebSocket** itself — every `LiveFrame` as it lands (Pro plans and up); returns an unsubscribe function |
52
54
  | `fight(id)` / `fightStats(id)` / `fightRounds(id)` | `GET /v1/fights/{id}` / `…/stats` / `…/rounds` |
53
55
  | `fightScorecards(id)` | `GET /v1/fights/{id}/scorecards` — judges, rounds, totals, deductions |
54
56
  | `judges({ q, org, minFights })` / `judge(id)` / `judgeScorecards(id)` | `GET /v1/judges` / `…/{id}` / `…/{id}/scorecards` |
@@ -63,6 +65,40 @@ List endpoints are **async generators** that follow `meta.pagination.next_cursor
63
65
 
64
66
  Hand-written types cover events, fights, fighters, rankings, scorecards and plans; the generated `paths` / `operations` types from the OpenAPI document are exported for everything else.
65
67
 
68
+ ## Live stream (UFC fight nights, Pro plans and up)
69
+
70
+ The **UFC live API**: a WebSocket that pushes the fight-night document the moment it
71
+ changes — card order and statuses, the bout in progress (round, running clock,
72
+ unofficial in-fight stats, per-round splits, a timestamped action timeline) and the
73
+ last result. It is the streaming half of `eventLive()`, and it is the **UFC live stats
74
+ API** you want instead of polling.
75
+
76
+ ```ts
77
+ import { FightAPI } from '@ufcalendar/sdk';
78
+
79
+ const api = new FightAPI(); // UFCAL_API_KEY
80
+
81
+ const stop = api.subscribeLive(
82
+ 'ufc-331',
83
+ (frame) => {
84
+ // frame.type: "snapshot" | "update" | "fight.final" | "event.completed"
85
+ const cur = frame.data?.current;
86
+ if (cur) console.log(frame.type, 'R', cur.round, cur.clock_sec);
87
+ },
88
+ { until: 'final' }, // close after the first fight.final; omit to run all night
89
+ );
90
+
91
+ // later
92
+ stop();
93
+ ```
94
+
95
+ `subscribeLive` uses the global `WebSocket` (browser, Node >= 22). On older Node inject
96
+ one: `{ WebSocketImpl: (await import('ws')).default as unknown as typeof WebSocket }`.
97
+ A dropped socket reconnects and re-subscribes once.
98
+
99
+ Runnable ticker: [`examples/live-ticker.ts`](examples/live-ticker.ts) — ~40 lines that
100
+ print `R2 3:41 · Oliveira 41 vs Makhachev 37 sig. strikes` as the round unfolds.
101
+
66
102
  ## Agents and MCP
67
103
 
68
104
  The same data is a Model Context Protocol server, so an agent can call it without an HTTP client:
package/dist/index.cjs CHANGED
@@ -23,15 +23,17 @@ __export(index_exports, {
23
23
  DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
24
24
  FightAPI: () => FightAPI,
25
25
  FightAPIError: () => FightAPIError,
26
+ LIVE_WS_URL: () => LIVE_WS_URL,
26
27
  VERSION: () => VERSION
27
28
  });
28
29
  module.exports = __toCommonJS(index_exports);
29
30
 
30
31
  // src/version.ts
31
- var VERSION = "0.2.0";
32
+ var VERSION = "0.3.0";
32
33
 
33
34
  // src/client.ts
34
35
  var DEFAULT_BASE_URL = "https://api.ufcalendar.com/v1";
36
+ var LIVE_WS_URL = "wss://live.ufcalendar.com/v1";
35
37
  var DEFAULT_TIMEOUT_MS = 3e4;
36
38
  var FightAPIError = class extends Error {
37
39
  status;
@@ -199,12 +201,87 @@ var FightAPI = class {
199
201
  eventChanges(idOrSlug) {
200
202
  return this.get(`events/${encodeURIComponent(String(idOrSlug))}/changes`);
201
203
  }
202
- /** Latest real-time LiveState snapshot on fight night (Business+), or null
203
- * when nothing is being streamed. The WebSocket at wss://live.ufcalendar.com/v1
204
- * pushes the same document as it changes. */
204
+ /** Latest real-time LiveState snapshot on fight night (Pro plans and up), or
205
+ * null when nothing is being streamed. The WebSocket at
206
+ * wss://live.ufcalendar.com/v1 pushes the same document as it changes — see
207
+ * `subscribeLive()`. */
205
208
  eventLive(idOrSlug) {
206
209
  return this.get(`events/${encodeURIComponent(String(idOrSlug))}/live`);
207
210
  }
211
+ /**
212
+ * Subscribe to the live WebSocket and receive every frame (Pro plans and up).
213
+ *
214
+ * The UFC live API: opens `wss://live.ufcalendar.com/v1?key=…`, sends
215
+ * `{"action":"subscribe","event":<slug>}` and calls `onFrame` with each
216
+ * `LiveFrame` — round, running clock, unofficial in-fight statistics and the
217
+ * action timeline, the same `LiveState` `eventLive()` returns.
218
+ *
219
+ * ```ts
220
+ * const stop = api.subscribeLive('ufc-331', (f) => console.log(f.type), { until: 'final' });
221
+ * ```
222
+ *
223
+ * Uses the global `WebSocket` (browser, Node >= 22) unless you inject one
224
+ * (`ws` on older Node). A dropped socket reconnects and re-subscribes ONCE.
225
+ *
226
+ * @returns an unsubscribe function — call it to close the socket for good.
227
+ */
228
+ subscribeLive(event, onFrame, opts = {}) {
229
+ const Impl = opts.WebSocketImpl ?? globalThis.WebSocket;
230
+ if (!Impl) {
231
+ throw new Error(
232
+ "subscribeLive() needs a WebSocket: use Node >= 22 (or a browser), or pass { WebSocketImpl } \u2014 e.g. `import WebSocket from 'ws'`."
233
+ );
234
+ }
235
+ if (!this.apiKey) {
236
+ throw new Error(
237
+ "No API key. The live stream authenticates with ?key=. Keys (and the free 1-day trial) live at https://www.ufcalendar.com/account/api"
238
+ );
239
+ }
240
+ const url = `${LIVE_WS_URL}?key=${encodeURIComponent(this.apiKey)}`;
241
+ let done = false;
242
+ let reconnects = 0;
243
+ let sock = null;
244
+ const open = () => {
245
+ const ws = new Impl(url);
246
+ sock = ws;
247
+ ws.onopen = () => {
248
+ try {
249
+ ws.send(JSON.stringify({ action: "subscribe", event }));
250
+ } catch {
251
+ }
252
+ };
253
+ ws.onmessage = (ev) => {
254
+ let frame;
255
+ try {
256
+ frame = JSON.parse(typeof ev.data === "string" ? ev.data : String(ev.data));
257
+ } catch {
258
+ return;
259
+ }
260
+ if (!frame || typeof frame !== "object") return;
261
+ onFrame(frame);
262
+ if (opts.until === "final" && frame.type === "fight.final") {
263
+ done = true;
264
+ try {
265
+ ws.close();
266
+ } catch {
267
+ }
268
+ }
269
+ };
270
+ ws.onclose = () => {
271
+ if (done || reconnects >= 1) return;
272
+ reconnects += 1;
273
+ open();
274
+ };
275
+ };
276
+ open();
277
+ return () => {
278
+ done = true;
279
+ try {
280
+ sock?.close();
281
+ } catch {
282
+ }
283
+ };
284
+ }
208
285
  /* -------------------------------------------------------------- fights */
209
286
  fight(fightId) {
210
287
  return this.get(`fights/${encodeURIComponent(String(fightId))}`);
@@ -364,6 +441,7 @@ var FightAPI = class {
364
441
  DEFAULT_BASE_URL,
365
442
  FightAPI,
366
443
  FightAPIError,
444
+ LIVE_WS_URL,
367
445
  VERSION
368
446
  });
369
447
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/version.ts","../src/client.ts"],"sourcesContent":["export { FightAPI, FightAPIError, DEFAULT_BASE_URL } from './client';\nexport type { FightAPIOptions, Query, QueryValue } from './client';\nexport { VERSION } from './version';\nexport type * from './types';\n/** The generated OpenAPI path map, for anything the hand-written types above\n * do not cover. Regenerated by `pnpm gen:types` from apps/api/openapi.json. */\nexport type { paths, operations } from './schema';\n","/** Kept in lockstep with package.json and the User-Agent by\n * `apps/api/src/surfaces.test.ts`. Bump all three together. */\nexport const VERSION = '0.2.0';\n","/**\n * Thin, dependency-free client for https://api.ufcalendar.com/v1.\n *\n * Every method returns the parsed `data` value of the JSON envelope\n * (`{\"data\": ..., \"meta\": ...}`); list endpoints are async generators that\n * follow cursor pagination for you. Errors throw `FightAPIError` carrying\n * the API's `code`, `message` and `requestId` — quote the request id when\n * you write to api@ufcalendar.com.\n *\n * Mirrors sdk/python/ufcalendar/client.py one method per endpoint, in\n * camelCase. The two clients are kept in lockstep by\n * `apps/api/src/surfaces.test.ts`.\n *\n * The API serves no betting odds, by design. Fighter `images` are Wikimedia\n * Commons / Creative Commons files — the `license` and `artist` fields you\n * receive must be displayed as a credit.\n */\nimport { VERSION } from './version';\nimport type {\n BroadcastRight,\n CareerBout,\n CareerStats,\n Envelope,\n EventChange,\n EventDetail,\n EventSummary,\n Fight,\n Fighter,\n FighterSummary,\n Judge,\n Meta,\n Org,\n Plans,\n PowerIndex,\n RankingsBoard,\n RateLimit,\n Scorecards,\n Usage,\n Venue,\n WebhookEndpoint,\n LiveState,\n} from './types';\n\nexport const DEFAULT_BASE_URL = 'https://api.ufcalendar.com/v1';\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport interface FightAPIOptions {\n /** `ufcalendar_…` key from https://www.ufcalendar.com/account/api. Falls back\n * to `UFCALENDAR_API_KEY` / `UFCAL_API_KEY` in the environment. Only\n * `plans()` works without one. */\n apiKey?: string;\n /** Override for testing; defaults to the production `/v1`. */\n baseUrl?: string;\n /** Inject a fetch (a test double, an instrumented fetch, undici). */\n fetch?: typeof fetch;\n /** Per-request timeout. Default 30s. */\n timeoutMs?: number;\n /** Extra headers on every request. */\n headers?: Record<string, string>;\n}\n\n/** An error response from the Fight API. */\nexport class FightAPIError extends Error {\n readonly status: number;\n readonly code: string;\n readonly requestId: string | null;\n\n constructor(status: number, code: string, message: string, requestId: string | null = null) {\n super(`${status} ${code}: ${message}${requestId ? ` (request_id=${requestId})` : ''}`);\n this.name = 'FightAPIError';\n this.status = status;\n this.code = code;\n this.requestId = requestId;\n }\n}\n\nexport type QueryValue = string | number | boolean | null | undefined;\nexport type Query = Record<string, QueryValue>;\n\ninterface PageOptions {\n /** Stop after this many rows (across pages). */\n limit?: number;\n}\n\n/**\n * A browser forbids setting User-Agent on fetch, so we only set ours\n * off-browser. The test is `document`, NOT `navigator`: Node 21+ ships a\n * global `navigator`, so a navigator check would silently drop the header\n * on every modern Node runtime — which is the one place it works.\n */\nfunction canSetUserAgent(): boolean {\n return typeof document === 'undefined' && typeof window === 'undefined';\n}\n\nfunction envKey(): string | undefined {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;\n return env?.UFCALENDAR_API_KEY || env?.UFCAL_API_KEY;\n}\n\nexport class FightAPI {\n readonly baseUrl: string;\n readonly apiKey: string | undefined;\n\n /** `meta` of the most recent response (pagination cursor, generated_at …). */\n lastMeta: Meta | null = null;\n /** `X-RateLimit-*` of the most recent response. */\n lastRateLimit: RateLimit = { limit: null, remaining: null, reset: null };\n\n readonly #fetch: typeof fetch;\n readonly #timeoutMs: number;\n readonly #headers: Record<string, string>;\n\n constructor(apiKeyOrOptions?: string | FightAPIOptions, options: FightAPIOptions = {}) {\n const opts: FightAPIOptions =\n typeof apiKeyOrOptions === 'string'\n ? { ...options, apiKey: apiKeyOrOptions }\n : { ...options, ...(apiKeyOrOptions ?? {}) };\n this.apiKey = opts.apiKey ?? envKey();\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n this.#fetch = opts.fetch ?? globalThis.fetch;\n this.#timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.#headers = opts.headers ?? {};\n if (!this.#fetch) {\n throw new Error('No global fetch available. Use Node 18+ or pass { fetch }.');\n }\n }\n\n /* ---------------------------------------------------------------- core */\n\n #url(path: string, params?: Query): string {\n const url = new URL(`${this.baseUrl}/${path.replace(/^\\/+/, '')}`);\n for (const [k, v] of Object.entries(params ?? {})) {\n if (v === undefined || v === null) continue;\n url.searchParams.set(k, String(v));\n }\n return url.toString();\n }\n\n async #request<T>(\n method: string,\n path: string,\n { params, body, open = false }: { params?: Query; body?: unknown; open?: boolean } = {},\n ): Promise<Envelope<T>> {\n if (!this.apiKey && !open) {\n throw new FightAPIError(\n 401,\n 'no_api_key',\n 'No API key. Pass new FightAPI(\"ufcalendar_...\") or set UFCALENDAR_API_KEY. ' +\n 'Keys (and the free 1-day trial) live at https://www.ufcalendar.com/account/api',\n );\n }\n const headers: Record<string, string> = { Accept: 'application/json', ...this.#headers };\n if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;\n if (canSetUserAgent()) headers['User-Agent'] = `ufcalendar-typescript/${VERSION}`;\n if (body !== undefined) headers['Content-Type'] = 'application/json';\n\n let res: Response;\n try {\n res = await this.#fetch(this.#url(path, params), {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: AbortSignal.timeout(this.#timeoutMs),\n });\n } catch (e) {\n const err = e as { name?: string; message?: string };\n if (err?.name === 'TimeoutError') {\n throw new FightAPIError(0, 'timeout', `Request to ${path} timed out after ${this.#timeoutMs}ms`);\n }\n throw new FightAPIError(0, 'network_error', err?.message ?? String(e));\n }\n\n this.lastRateLimit = {\n limit: res.headers.get('X-RateLimit-Limit'),\n remaining: res.headers.get('X-RateLimit-Remaining'),\n reset: res.headers.get('X-RateLimit-Reset'),\n };\n\n if (res.status === 204) {\n this.lastMeta = null;\n return { data: undefined as T };\n }\n\n const text = await res.text();\n let parsed: unknown = undefined;\n try {\n parsed = text ? JSON.parse(text) : undefined;\n } catch {\n parsed = undefined;\n }\n\n if (!res.ok) {\n const err = (parsed as { error?: { code?: string; message?: string; request_id?: string } } | undefined)?.error;\n if (err) {\n throw new FightAPIError(\n res.status,\n String(err.code ?? 'error'),\n String(err.message ?? text.slice(0, 200)),\n err.request_id ?? res.headers.get('x-request-id'),\n );\n }\n throw new FightAPIError(res.status, 'http_error', text.slice(0, 200), res.headers.get('x-request-id'));\n }\n\n const env = (parsed ?? {}) as Envelope<T>;\n this.lastMeta = env.meta ?? null;\n return env;\n }\n\n /** Raw GET returning the `data` value. Escape hatch for new endpoints. */\n async get<T = unknown>(path: string, params?: Query): Promise<T> {\n return (await this.#request<T>('GET', path, { params })).data;\n }\n\n /** Raw GET returning the whole `{data, meta}` envelope. */\n async getWithMeta<T = unknown>(path: string, params?: Query): Promise<Envelope<T>> {\n return this.#request<T>('GET', path, { params });\n }\n\n async *#paginate<T>(path: string, params: Query, opts: PageOptions = {}): AsyncGenerator<T, void, void> {\n // Never fetch a bigger page than the caller wants rows: `limit: 3` used\n // to pull 100 rows and throw 97 away, burning the caller's quota on a\n // request they did not ask for (and, on a trial key, a noticeable slice\n // of the 100 they get).\n const pageSize = Math.min(100, opts.limit ?? 100);\n const query: Query = { limit: pageSize, ...params };\n let seen = 0;\n for (;;) {\n const env = await this.#request<T[]>('GET', path, { params: query });\n for (const row of env.data ?? []) {\n yield row;\n seen += 1;\n if (opts.limit !== undefined && seen >= opts.limit) return;\n }\n const cursor = env.meta?.pagination?.next_cursor;\n if (!cursor) return;\n query.cursor = cursor;\n }\n }\n\n /* --------------------------------------------------------------- plans */\n\n /**\n * Plans, quotas, the free 1-day trial rule, the MCP endpoint and the doc\n * links. The only endpoint that answers without a credential.\n */\n async plans(): Promise<Plans> {\n return (await this.#request<Plans>('GET', 'plans', { open: true })).data;\n }\n\n /* ---------------------------------------------------------------- orgs */\n\n /** Launch orgs with capability flags (stats / rounds / rankings / broadcasts / predictions). */\n orgs(): Promise<Org[]> {\n return this.get<Org[]>('orgs');\n }\n\n org(slug: string): Promise<Org> {\n return this.get<Org>(`orgs/${encodeURIComponent(slug)}`);\n }\n\n /* -------------------------------------------------------------- events */\n\n /**\n * Schedule + results. Bare call = the upcoming calendar, soonest first.\n * `status: 'completed'` (or `order: 'desc'`) browses the archive\n * newest-first. `from` / `to` are `YYYY-MM-DD`.\n */\n events(\n opts: {\n org?: string;\n status?: string;\n from?: string;\n to?: string;\n order?: 'asc' | 'desc';\n limit?: number;\n } = {},\n ): AsyncGenerator<EventSummary, void, void> {\n const { limit, ...params } = opts;\n return this.#paginate<EventSummary>('events', params as Query, { limit });\n }\n\n /** One event with its full fight card, venue and broadcasts. */\n event(idOrSlug: string | number): Promise<EventDetail> {\n return this.get<EventDetail>(`events/${encodeURIComponent(String(idOrSlug))}`);\n }\n\n /** Card-change diff log (fight added/removed, opponent swapped, date moved, profile merged). */\n eventChanges(idOrSlug: string | number): Promise<EventChange[]> {\n return this.get<EventChange[]>(`events/${encodeURIComponent(String(idOrSlug))}/changes`);\n }\n\n /** Latest real-time LiveState snapshot on fight night (Business+), or null\n * when nothing is being streamed. The WebSocket at wss://live.ufcalendar.com/v1\n * pushes the same document as it changes. */\n eventLive(idOrSlug: string | number): Promise<LiveState | null> {\n return this.get<LiveState | null>(`events/${encodeURIComponent(String(idOrSlug))}/live`);\n }\n\n /* -------------------------------------------------------------- fights */\n\n fight(fightId: number | string): Promise<Fight> {\n return this.get<Fight>(`fights/${encodeURIComponent(String(fightId))}`);\n }\n\n /** Per-fight totals for both corners. */\n fightStats(fightId: number | string): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>(`fights/${encodeURIComponent(String(fightId))}/stats`);\n }\n\n /** Round-by-round stat lines for both corners. */\n fightRounds(fightId: number | string): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>(`fights/${encodeURIComponent(String(fightId))}/rounds`);\n }\n\n /**\n * The judges' scorecards for a bout — the official commission record:\n * decision type, point deductions, and one card per judge with every\n * round, the totals and `winner_fighter_id`. Scores are oriented to\n * `fighter_a_id` / `fighter_b_id`, both repeated on the payload.\n *\n * Check `scores_known` on a card before charting totals: when it is false\n * the commission published only the outcome. Throws 404 for a bout that\n * did not go to the judges.\n */\n fightScorecards(fightId: number | string): Promise<Scorecards> {\n return this.get<Scorecards>(`fights/${encodeURIComponent(String(fightId))}/scorecards`);\n }\n\n /* -------------------------------------------------------------- judges */\n\n /**\n * Every official who has scored a launch-org bout, busiest first. Rates\n * mean little below ~10 fights; pass `minFights: 10`.\n */\n judges(\n opts: { q?: string; org?: string; minFights?: number; limit?: number } = {},\n ): AsyncGenerator<Judge, void, void> {\n const { limit, minFights, ...rest } = opts;\n return this.#paginate<Judge>('judges', { ...rest, min_fights: minFights } as Query, { limit });\n }\n\n judge(judgeId: number | string): Promise<Judge> {\n return this.get<Judge>(`judges/${encodeURIComponent(String(judgeId))}`);\n }\n\n /** Every card this judge has turned in, newest first. */\n judgeScorecards(\n judgeId: number | string,\n opts: { limit?: number } = {},\n ): AsyncGenerator<Record<string, unknown>, void, void> {\n return this.#paginate<Record<string, unknown>>(\n `judges/${encodeURIComponent(String(judgeId))}/scorecards`,\n {},\n opts,\n );\n }\n\n /* ------------------------------------------------------------ fighters */\n\n fighters(\n opts: { q?: string; org?: string; country?: string; limit?: number } = {},\n ): AsyncGenerator<FighterSummary, void, void> {\n const { limit, ...params } = opts;\n return this.#paginate<FighterSummary>('fighters', params as Query, { limit });\n }\n\n /** Bio, records, career stats, Power Index and CC-licensed images. */\n fighter(idOrSlug: string | number): Promise<Fighter> {\n return this.get<Fighter>(`fighters/${encodeURIComponent(String(idOrSlug))}`);\n }\n\n /** Complete multi-promotion career timeline. */\n fighterHistory(idOrSlug: string | number): Promise<CareerBout[]> {\n return this.get<CareerBout[]>(`fighters/${encodeURIComponent(String(idOrSlug))}/history`);\n }\n\n /** Career statistics, one source-stamped row per scope (`pro-mma`, `ufc-only` …). */\n fighterStats(idOrSlug: string | number): Promise<CareerStats[]> {\n return this.get<CareerStats[]>(`fighters/${encodeURIComponent(String(idOrSlug))}/stats`);\n }\n\n /** Every official ranking row the fighter ever held, newest first. */\n fighterRankings(idOrSlug: string | number): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>(`fighters/${encodeURIComponent(String(idOrSlug))}/rankings`);\n }\n\n fighterPowerIndex(idOrSlug: string | number): Promise<PowerIndex & Record<string, unknown>> {\n return this.get<PowerIndex & Record<string, unknown>>(\n `fighters/${encodeURIComponent(String(idOrSlug))}/power-index`,\n );\n }\n\n /* ------------------------------------------------------------ rankings */\n\n /**\n * Official board, point-in-time. `date: 'YYYY-MM-DD'` returns the board\n * that was valid on that day (UFC history back to 2013; rank 0 = champion).\n */\n rankings(org = 'ufc', opts: { date?: string; board?: string } = {}): Promise<RankingsBoard> {\n return this.get<RankingsBoard>(`rankings/${encodeURIComponent(org)}`, opts as Query);\n }\n\n divisionRankings(org: string, division: string, opts: { date?: string } = {}): Promise<RankingsBoard> {\n return this.get<RankingsBoard>(\n `rankings/${encodeURIComponent(org)}/${encodeURIComponent(division)}`,\n opts as Query,\n );\n }\n\n /** Current champions across every launch org. */\n champions(): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>('champions');\n }\n\n /** UFCalendar Power Index board. */\n powerIndex(org = 'ufc'): Promise<Record<string, unknown>> {\n return this.get<Record<string, unknown>>(`power-index/${encodeURIComponent(org)}`);\n }\n\n /* ---------------------------------------------------------------- misc */\n\n /** Model win probabilities for upcoming UFC bouts. */\n predictionsUpcoming(): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>('predictions/upcoming');\n }\n\n /** Who airs the promotion, per ISO-2 country. */\n broadcastRights(org = 'ufc', opts: { country?: string } = {}): Promise<BroadcastRight[]> {\n return this.get<BroadcastRight[]>(`broadcast-rights/${encodeURIComponent(org)}`, opts as Query);\n }\n\n venue(venueId: number | string): Promise<Venue> {\n return this.get<Venue>(`venues/${encodeURIComponent(String(venueId))}`);\n }\n\n /** Typeahead across fighters and events. */\n search(q: string): Promise<Record<string, unknown>> {\n return this.get<Record<string, unknown>>('search', { q });\n }\n\n /** Your key's month-to-date quota usage. */\n usage(): Promise<Usage> {\n return this.get<Usage>('usage');\n }\n\n /**\n * Subscribable ICS feed URL for calendar apps (authenticates via `?key=`).\n *\n * Throws without a key rather than returning `?key=`: that empty URL is\n * pasted into a calendar app, fails there hours later, and the failure\n * surfaces nowhere near this call.\n */\n calendarIcsUrl(org = 'ufc'): string {\n if (!this.apiKey) {\n throw new FightAPIError(\n 401,\n 'no_api_key',\n 'No API key. Pass new FightAPI(\"ufcalendar_...\") or set UFCALENDAR_API_KEY. ' +\n 'Keys (and the free 1-day trial) live at https://www.ufcalendar.com/account/api',\n );\n }\n return `${this.baseUrl}/calendar/${encodeURIComponent(org)}.ics?key=${encodeURIComponent(this.apiKey)}`;\n }\n\n /* ------------------------------------------------------------ webhooks */\n\n webhookEndpoints(): Promise<WebhookEndpoint[]> {\n return this.get<WebhookEndpoint[]>('webhook-endpoints');\n }\n\n /**\n * Register a signed webhook (Pro and up). `events` ⊆ `event.announced`,\n * `fight.result`, `card.changed`, `event.completed`. The signing secret is\n * returned ONCE, in this response.\n */\n async createWebhookEndpoint(url: string, events?: string[]): Promise<WebhookEndpoint> {\n const body: Record<string, unknown> = { url };\n if (events?.length) body.events = events;\n return (await this.#request<WebhookEndpoint>('POST', 'webhook-endpoints', { body })).data;\n }\n\n async deleteWebhookEndpoint(endpointId: number | string): Promise<void> {\n await this.#request<unknown>('DELETE', `webhook-endpoints/${encodeURIComponent(String(endpointId))}`);\n }\n\n async rotateWebhookSecret(endpointId: number | string): Promise<WebhookEndpoint> {\n return (\n await this.#request<WebhookEndpoint>(\n 'POST',\n `webhook-endpoints/${encodeURIComponent(String(endpointId))}/rotate-secret`,\n )\n ).data;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/version.ts","../src/client.ts"],"sourcesContent":["export { FightAPI, FightAPIError, DEFAULT_BASE_URL, LIVE_WS_URL } from './client';\nexport type { FightAPIOptions, Query, QueryValue } from './client';\nexport { VERSION } from './version';\nexport type * from './types';\n/** The generated OpenAPI path map, for anything the hand-written types above\n * do not cover. Regenerated by `pnpm gen:types` from apps/api/openapi.json. */\nexport type { paths, operations } from './schema';\n","/** Kept in lockstep with package.json and the User-Agent by\n * `apps/api/src/surfaces.test.ts`. Bump all three together. */\nexport const VERSION = '0.3.0';\n","/**\n * Thin, dependency-free client for https://api.ufcalendar.com/v1.\n *\n * Every method returns the parsed `data` value of the JSON envelope\n * (`{\"data\": ..., \"meta\": ...}`); list endpoints are async generators that\n * follow cursor pagination for you. Errors throw `FightAPIError` carrying\n * the API's `code`, `message` and `requestId` — quote the request id when\n * you write to api@ufcalendar.com.\n *\n * Mirrors sdk/python/ufcalendar/client.py one method per endpoint, in\n * camelCase. The two clients are kept in lockstep by\n * `apps/api/src/surfaces.test.ts`.\n *\n * The API serves no betting odds, by design. Fighter `images` are Wikimedia\n * Commons / Creative Commons files — the `license` and `artist` fields you\n * receive must be displayed as a credit.\n */\nimport { VERSION } from './version';\nimport type {\n BroadcastRight,\n CareerBout,\n CareerStats,\n Envelope,\n EventChange,\n EventDetail,\n EventSummary,\n Fight,\n Fighter,\n FighterSummary,\n Judge,\n Meta,\n Org,\n Plans,\n PowerIndex,\n RankingsBoard,\n RateLimit,\n Scorecards,\n Usage,\n Venue,\n WebhookEndpoint,\n LiveState,\n LiveFrame,\n} from './types';\n\nexport const DEFAULT_BASE_URL = 'https://api.ufcalendar.com/v1';\n/** The live WebSocket (UFC fight nights, Pro plans and up) — the same document\n * `GET /v1/events/{id}/live` returns, pushed on every change. */\nexport const LIVE_WS_URL = 'wss://live.ufcalendar.com/v1';\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport interface FightAPIOptions {\n /** `ufcalendar_…` key from https://www.ufcalendar.com/account/api. Falls back\n * to `UFCALENDAR_API_KEY` / `UFCAL_API_KEY` in the environment. Only\n * `plans()` works without one. */\n apiKey?: string;\n /** Override for testing; defaults to the production `/v1`. */\n baseUrl?: string;\n /** Inject a fetch (a test double, an instrumented fetch, undici). */\n fetch?: typeof fetch;\n /** Per-request timeout. Default 30s. */\n timeoutMs?: number;\n /** Extra headers on every request. */\n headers?: Record<string, string>;\n}\n\n/** An error response from the Fight API. */\nexport class FightAPIError extends Error {\n readonly status: number;\n readonly code: string;\n readonly requestId: string | null;\n\n constructor(status: number, code: string, message: string, requestId: string | null = null) {\n super(`${status} ${code}: ${message}${requestId ? ` (request_id=${requestId})` : ''}`);\n this.name = 'FightAPIError';\n this.status = status;\n this.code = code;\n this.requestId = requestId;\n }\n}\n\nexport type QueryValue = string | number | boolean | null | undefined;\nexport type Query = Record<string, QueryValue>;\n\ninterface PageOptions {\n /** Stop after this many rows (across pages). */\n limit?: number;\n}\n\n/**\n * A browser forbids setting User-Agent on fetch, so we only set ours\n * off-browser. The test is `document`, NOT `navigator`: Node 21+ ships a\n * global `navigator`, so a navigator check would silently drop the header\n * on every modern Node runtime — which is the one place it works.\n */\nfunction canSetUserAgent(): boolean {\n return typeof document === 'undefined' && typeof window === 'undefined';\n}\n\nfunction envKey(): string | undefined {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;\n return env?.UFCALENDAR_API_KEY || env?.UFCAL_API_KEY;\n}\n\nexport class FightAPI {\n readonly baseUrl: string;\n readonly apiKey: string | undefined;\n\n /** `meta` of the most recent response (pagination cursor, generated_at …). */\n lastMeta: Meta | null = null;\n /** `X-RateLimit-*` of the most recent response. */\n lastRateLimit: RateLimit = { limit: null, remaining: null, reset: null };\n\n readonly #fetch: typeof fetch;\n readonly #timeoutMs: number;\n readonly #headers: Record<string, string>;\n\n constructor(apiKeyOrOptions?: string | FightAPIOptions, options: FightAPIOptions = {}) {\n const opts: FightAPIOptions =\n typeof apiKeyOrOptions === 'string'\n ? { ...options, apiKey: apiKeyOrOptions }\n : { ...options, ...(apiKeyOrOptions ?? {}) };\n this.apiKey = opts.apiKey ?? envKey();\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n this.#fetch = opts.fetch ?? globalThis.fetch;\n this.#timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.#headers = opts.headers ?? {};\n if (!this.#fetch) {\n throw new Error('No global fetch available. Use Node 18+ or pass { fetch }.');\n }\n }\n\n /* ---------------------------------------------------------------- core */\n\n #url(path: string, params?: Query): string {\n const url = new URL(`${this.baseUrl}/${path.replace(/^\\/+/, '')}`);\n for (const [k, v] of Object.entries(params ?? {})) {\n if (v === undefined || v === null) continue;\n url.searchParams.set(k, String(v));\n }\n return url.toString();\n }\n\n async #request<T>(\n method: string,\n path: string,\n { params, body, open = false }: { params?: Query; body?: unknown; open?: boolean } = {},\n ): Promise<Envelope<T>> {\n if (!this.apiKey && !open) {\n throw new FightAPIError(\n 401,\n 'no_api_key',\n 'No API key. Pass new FightAPI(\"ufcalendar_...\") or set UFCALENDAR_API_KEY. ' +\n 'Keys (and the free 1-day trial) live at https://www.ufcalendar.com/account/api',\n );\n }\n const headers: Record<string, string> = { Accept: 'application/json', ...this.#headers };\n if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;\n if (canSetUserAgent()) headers['User-Agent'] = `ufcalendar-typescript/${VERSION}`;\n if (body !== undefined) headers['Content-Type'] = 'application/json';\n\n let res: Response;\n try {\n res = await this.#fetch(this.#url(path, params), {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: AbortSignal.timeout(this.#timeoutMs),\n });\n } catch (e) {\n const err = e as { name?: string; message?: string };\n if (err?.name === 'TimeoutError') {\n throw new FightAPIError(0, 'timeout', `Request to ${path} timed out after ${this.#timeoutMs}ms`);\n }\n throw new FightAPIError(0, 'network_error', err?.message ?? String(e));\n }\n\n this.lastRateLimit = {\n limit: res.headers.get('X-RateLimit-Limit'),\n remaining: res.headers.get('X-RateLimit-Remaining'),\n reset: res.headers.get('X-RateLimit-Reset'),\n };\n\n if (res.status === 204) {\n this.lastMeta = null;\n return { data: undefined as T };\n }\n\n const text = await res.text();\n let parsed: unknown = undefined;\n try {\n parsed = text ? JSON.parse(text) : undefined;\n } catch {\n parsed = undefined;\n }\n\n if (!res.ok) {\n const err = (parsed as { error?: { code?: string; message?: string; request_id?: string } } | undefined)?.error;\n if (err) {\n throw new FightAPIError(\n res.status,\n String(err.code ?? 'error'),\n String(err.message ?? text.slice(0, 200)),\n err.request_id ?? res.headers.get('x-request-id'),\n );\n }\n throw new FightAPIError(res.status, 'http_error', text.slice(0, 200), res.headers.get('x-request-id'));\n }\n\n const env = (parsed ?? {}) as Envelope<T>;\n this.lastMeta = env.meta ?? null;\n return env;\n }\n\n /** Raw GET returning the `data` value. Escape hatch for new endpoints. */\n async get<T = unknown>(path: string, params?: Query): Promise<T> {\n return (await this.#request<T>('GET', path, { params })).data;\n }\n\n /** Raw GET returning the whole `{data, meta}` envelope. */\n async getWithMeta<T = unknown>(path: string, params?: Query): Promise<Envelope<T>> {\n return this.#request<T>('GET', path, { params });\n }\n\n async *#paginate<T>(path: string, params: Query, opts: PageOptions = {}): AsyncGenerator<T, void, void> {\n // Never fetch a bigger page than the caller wants rows: `limit: 3` used\n // to pull 100 rows and throw 97 away, burning the caller's quota on a\n // request they did not ask for (and, on a trial key, a noticeable slice\n // of the 100 they get).\n const pageSize = Math.min(100, opts.limit ?? 100);\n const query: Query = { limit: pageSize, ...params };\n let seen = 0;\n for (;;) {\n const env = await this.#request<T[]>('GET', path, { params: query });\n for (const row of env.data ?? []) {\n yield row;\n seen += 1;\n if (opts.limit !== undefined && seen >= opts.limit) return;\n }\n const cursor = env.meta?.pagination?.next_cursor;\n if (!cursor) return;\n query.cursor = cursor;\n }\n }\n\n /* --------------------------------------------------------------- plans */\n\n /**\n * Plans, quotas, the free 1-day trial rule, the MCP endpoint and the doc\n * links. The only endpoint that answers without a credential.\n */\n async plans(): Promise<Plans> {\n return (await this.#request<Plans>('GET', 'plans', { open: true })).data;\n }\n\n /* ---------------------------------------------------------------- orgs */\n\n /** Launch orgs with capability flags (stats / rounds / rankings / broadcasts / predictions). */\n orgs(): Promise<Org[]> {\n return this.get<Org[]>('orgs');\n }\n\n org(slug: string): Promise<Org> {\n return this.get<Org>(`orgs/${encodeURIComponent(slug)}`);\n }\n\n /* -------------------------------------------------------------- events */\n\n /**\n * Schedule + results. Bare call = the upcoming calendar, soonest first.\n * `status: 'completed'` (or `order: 'desc'`) browses the archive\n * newest-first. `from` / `to` are `YYYY-MM-DD`.\n */\n events(\n opts: {\n org?: string;\n status?: string;\n from?: string;\n to?: string;\n order?: 'asc' | 'desc';\n limit?: number;\n } = {},\n ): AsyncGenerator<EventSummary, void, void> {\n const { limit, ...params } = opts;\n return this.#paginate<EventSummary>('events', params as Query, { limit });\n }\n\n /** One event with its full fight card, venue and broadcasts. */\n event(idOrSlug: string | number): Promise<EventDetail> {\n return this.get<EventDetail>(`events/${encodeURIComponent(String(idOrSlug))}`);\n }\n\n /** Card-change diff log (fight added/removed, opponent swapped, date moved, profile merged). */\n eventChanges(idOrSlug: string | number): Promise<EventChange[]> {\n return this.get<EventChange[]>(`events/${encodeURIComponent(String(idOrSlug))}/changes`);\n }\n\n /** Latest real-time LiveState snapshot on fight night (Pro plans and up), or\n * null when nothing is being streamed. The WebSocket at\n * wss://live.ufcalendar.com/v1 pushes the same document as it changes — see\n * `subscribeLive()`. */\n eventLive(idOrSlug: string | number): Promise<LiveState | null> {\n return this.get<LiveState | null>(`events/${encodeURIComponent(String(idOrSlug))}/live`);\n }\n\n /**\n * Subscribe to the live WebSocket and receive every frame (Pro plans and up).\n *\n * The UFC live API: opens `wss://live.ufcalendar.com/v1?key=…`, sends\n * `{\"action\":\"subscribe\",\"event\":<slug>}` and calls `onFrame` with each\n * `LiveFrame` — round, running clock, unofficial in-fight statistics and the\n * action timeline, the same `LiveState` `eventLive()` returns.\n *\n * ```ts\n * const stop = api.subscribeLive('ufc-331', (f) => console.log(f.type), { until: 'final' });\n * ```\n *\n * Uses the global `WebSocket` (browser, Node >= 22) unless you inject one\n * (`ws` on older Node). A dropped socket reconnects and re-subscribes ONCE.\n *\n * @returns an unsubscribe function — call it to close the socket for good.\n */\n subscribeLive(\n event: string,\n onFrame: (frame: LiveFrame) => void,\n opts: { until?: 'final'; WebSocketImpl?: typeof WebSocket } = {},\n ): () => void {\n const Impl = opts.WebSocketImpl ?? (globalThis as { WebSocket?: typeof WebSocket }).WebSocket;\n if (!Impl) {\n throw new Error(\n 'subscribeLive() needs a WebSocket: use Node >= 22 (or a browser), or pass ' +\n \"{ WebSocketImpl } — e.g. `import WebSocket from 'ws'`.\",\n );\n }\n if (!this.apiKey) {\n throw new Error(\n 'No API key. The live stream authenticates with ?key=. Keys (and the free ' +\n '1-day trial) live at https://www.ufcalendar.com/account/api',\n );\n }\n const url = `${LIVE_WS_URL}?key=${encodeURIComponent(this.apiKey)}`;\n let done = false;\n let reconnects = 0;\n let sock: WebSocket | null = null;\n\n const open = () => {\n const ws = new Impl(url);\n sock = ws;\n ws.onopen = () => {\n try {\n ws.send(JSON.stringify({ action: 'subscribe', event }));\n } catch {\n /* the close handler reconnects */\n }\n };\n ws.onmessage = (ev: MessageEvent) => {\n let frame: LiveFrame;\n try {\n frame = JSON.parse(typeof ev.data === 'string' ? ev.data : String(ev.data)) as LiveFrame;\n } catch {\n return; // a half-frame is not worth killing the night over\n }\n if (!frame || typeof frame !== 'object') return;\n onFrame(frame);\n if (opts.until === 'final' && frame.type === 'fight.final') {\n done = true;\n try {\n ws.close();\n } catch {\n /* already gone */\n }\n }\n };\n ws.onclose = () => {\n if (done || reconnects >= 1) return;\n reconnects += 1;\n open();\n };\n };\n open();\n\n return () => {\n done = true;\n try {\n sock?.close();\n } catch {\n /* already gone */\n }\n };\n }\n\n /* -------------------------------------------------------------- fights */\n\n fight(fightId: number | string): Promise<Fight> {\n return this.get<Fight>(`fights/${encodeURIComponent(String(fightId))}`);\n }\n\n /** Per-fight totals for both corners. */\n fightStats(fightId: number | string): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>(`fights/${encodeURIComponent(String(fightId))}/stats`);\n }\n\n /** Round-by-round stat lines for both corners. */\n fightRounds(fightId: number | string): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>(`fights/${encodeURIComponent(String(fightId))}/rounds`);\n }\n\n /**\n * The judges' scorecards for a bout — the official commission record:\n * decision type, point deductions, and one card per judge with every\n * round, the totals and `winner_fighter_id`. Scores are oriented to\n * `fighter_a_id` / `fighter_b_id`, both repeated on the payload.\n *\n * Check `scores_known` on a card before charting totals: when it is false\n * the commission published only the outcome. Throws 404 for a bout that\n * did not go to the judges.\n */\n fightScorecards(fightId: number | string): Promise<Scorecards> {\n return this.get<Scorecards>(`fights/${encodeURIComponent(String(fightId))}/scorecards`);\n }\n\n /* -------------------------------------------------------------- judges */\n\n /**\n * Every official who has scored a launch-org bout, busiest first. Rates\n * mean little below ~10 fights; pass `minFights: 10`.\n */\n judges(\n opts: { q?: string; org?: string; minFights?: number; limit?: number } = {},\n ): AsyncGenerator<Judge, void, void> {\n const { limit, minFights, ...rest } = opts;\n return this.#paginate<Judge>('judges', { ...rest, min_fights: minFights } as Query, { limit });\n }\n\n judge(judgeId: number | string): Promise<Judge> {\n return this.get<Judge>(`judges/${encodeURIComponent(String(judgeId))}`);\n }\n\n /** Every card this judge has turned in, newest first. */\n judgeScorecards(\n judgeId: number | string,\n opts: { limit?: number } = {},\n ): AsyncGenerator<Record<string, unknown>, void, void> {\n return this.#paginate<Record<string, unknown>>(\n `judges/${encodeURIComponent(String(judgeId))}/scorecards`,\n {},\n opts,\n );\n }\n\n /* ------------------------------------------------------------ fighters */\n\n fighters(\n opts: { q?: string; org?: string; country?: string; limit?: number } = {},\n ): AsyncGenerator<FighterSummary, void, void> {\n const { limit, ...params } = opts;\n return this.#paginate<FighterSummary>('fighters', params as Query, { limit });\n }\n\n /** Bio, records, career stats, Power Index and CC-licensed images. */\n fighter(idOrSlug: string | number): Promise<Fighter> {\n return this.get<Fighter>(`fighters/${encodeURIComponent(String(idOrSlug))}`);\n }\n\n /** Complete multi-promotion career timeline. */\n fighterHistory(idOrSlug: string | number): Promise<CareerBout[]> {\n return this.get<CareerBout[]>(`fighters/${encodeURIComponent(String(idOrSlug))}/history`);\n }\n\n /** Career statistics, one source-stamped row per scope (`pro-mma`, `ufc-only` …). */\n fighterStats(idOrSlug: string | number): Promise<CareerStats[]> {\n return this.get<CareerStats[]>(`fighters/${encodeURIComponent(String(idOrSlug))}/stats`);\n }\n\n /** Every official ranking row the fighter ever held, newest first. */\n fighterRankings(idOrSlug: string | number): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>(`fighters/${encodeURIComponent(String(idOrSlug))}/rankings`);\n }\n\n fighterPowerIndex(idOrSlug: string | number): Promise<PowerIndex & Record<string, unknown>> {\n return this.get<PowerIndex & Record<string, unknown>>(\n `fighters/${encodeURIComponent(String(idOrSlug))}/power-index`,\n );\n }\n\n /* ------------------------------------------------------------ rankings */\n\n /**\n * Official board, point-in-time. `date: 'YYYY-MM-DD'` returns the board\n * that was valid on that day (UFC history back to 2013; rank 0 = champion).\n */\n rankings(org = 'ufc', opts: { date?: string; board?: string } = {}): Promise<RankingsBoard> {\n return this.get<RankingsBoard>(`rankings/${encodeURIComponent(org)}`, opts as Query);\n }\n\n divisionRankings(org: string, division: string, opts: { date?: string } = {}): Promise<RankingsBoard> {\n return this.get<RankingsBoard>(\n `rankings/${encodeURIComponent(org)}/${encodeURIComponent(division)}`,\n opts as Query,\n );\n }\n\n /** Current champions across every launch org. */\n champions(): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>('champions');\n }\n\n /** UFCalendar Power Index board. */\n powerIndex(org = 'ufc'): Promise<Record<string, unknown>> {\n return this.get<Record<string, unknown>>(`power-index/${encodeURIComponent(org)}`);\n }\n\n /* ---------------------------------------------------------------- misc */\n\n /** Model win probabilities for upcoming UFC bouts. */\n predictionsUpcoming(): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>('predictions/upcoming');\n }\n\n /** Who airs the promotion, per ISO-2 country. */\n broadcastRights(org = 'ufc', opts: { country?: string } = {}): Promise<BroadcastRight[]> {\n return this.get<BroadcastRight[]>(`broadcast-rights/${encodeURIComponent(org)}`, opts as Query);\n }\n\n venue(venueId: number | string): Promise<Venue> {\n return this.get<Venue>(`venues/${encodeURIComponent(String(venueId))}`);\n }\n\n /** Typeahead across fighters and events. */\n search(q: string): Promise<Record<string, unknown>> {\n return this.get<Record<string, unknown>>('search', { q });\n }\n\n /** Your key's month-to-date quota usage. */\n usage(): Promise<Usage> {\n return this.get<Usage>('usage');\n }\n\n /**\n * Subscribable ICS feed URL for calendar apps (authenticates via `?key=`).\n *\n * Throws without a key rather than returning `?key=`: that empty URL is\n * pasted into a calendar app, fails there hours later, and the failure\n * surfaces nowhere near this call.\n */\n calendarIcsUrl(org = 'ufc'): string {\n if (!this.apiKey) {\n throw new FightAPIError(\n 401,\n 'no_api_key',\n 'No API key. Pass new FightAPI(\"ufcalendar_...\") or set UFCALENDAR_API_KEY. ' +\n 'Keys (and the free 1-day trial) live at https://www.ufcalendar.com/account/api',\n );\n }\n return `${this.baseUrl}/calendar/${encodeURIComponent(org)}.ics?key=${encodeURIComponent(this.apiKey)}`;\n }\n\n /* ------------------------------------------------------------ webhooks */\n\n webhookEndpoints(): Promise<WebhookEndpoint[]> {\n return this.get<WebhookEndpoint[]>('webhook-endpoints');\n }\n\n /**\n * Register a signed webhook (Pro and up). `events` ⊆ `event.announced`,\n * `fight.result`, `card.changed`, `event.completed`. The signing secret is\n * returned ONCE, in this response.\n */\n async createWebhookEndpoint(url: string, events?: string[]): Promise<WebhookEndpoint> {\n const body: Record<string, unknown> = { url };\n if (events?.length) body.events = events;\n return (await this.#request<WebhookEndpoint>('POST', 'webhook-endpoints', { body })).data;\n }\n\n async deleteWebhookEndpoint(endpointId: number | string): Promise<void> {\n await this.#request<unknown>('DELETE', `webhook-endpoints/${encodeURIComponent(String(endpointId))}`);\n }\n\n async rotateWebhookSecret(endpointId: number | string): Promise<WebhookEndpoint> {\n return (\n await this.#request<WebhookEndpoint>(\n 'POST',\n `webhook-endpoints/${encodeURIComponent(String(endpointId))}/rotate-secret`,\n )\n ).data;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -183,6 +183,22 @@ interface LiveState {
183
183
  time: string | null;
184
184
  } | null;
185
185
  }
186
+ /** One frame off the live WebSocket (`wss://live.ufcalendar.com/v1`).
187
+ *
188
+ * `snapshot` arrives on subscribe (`data` is null with `reason: "not_live"`
189
+ * when nothing is streaming yet), `update` on every change, `fight.final`
190
+ * when a bout is official and `event.completed` when the card is over. */
191
+ interface LiveFrame {
192
+ type: 'snapshot' | 'update' | 'fight.final' | 'event.completed' | 'pong' | 'error';
193
+ data?: LiveState | null;
194
+ /** On a `snapshot` with no document: why (`not_live`). */
195
+ reason?: string;
196
+ /** On `error`. */
197
+ code?: string;
198
+ message?: string;
199
+ /** On `pong`. */
200
+ ts?: number;
201
+ }
186
202
  /** Identity fields only — the full record is on `/v1/fighters/{id}`. */
187
203
  interface FighterRef {
188
204
  id: number;
@@ -400,6 +416,9 @@ interface Usage {
400
416
  }
401
417
 
402
418
  declare const DEFAULT_BASE_URL = "https://api.ufcalendar.com/v1";
419
+ /** The live WebSocket (UFC fight nights, Pro plans and up) — the same document
420
+ * `GET /v1/events/{id}/live` returns, pushed on every change. */
421
+ declare const LIVE_WS_URL = "wss://live.ufcalendar.com/v1";
403
422
  interface FightAPIOptions {
404
423
  /** `ufcalendar_…` key from https://www.ufcalendar.com/account/api. Falls back
405
424
  * to `UFCALENDAR_API_KEY` / `UFCAL_API_KEY` in the environment. Only
@@ -461,10 +480,32 @@ declare class FightAPI {
461
480
  event(idOrSlug: string | number): Promise<EventDetail>;
462
481
  /** Card-change diff log (fight added/removed, opponent swapped, date moved, profile merged). */
463
482
  eventChanges(idOrSlug: string | number): Promise<EventChange[]>;
464
- /** Latest real-time LiveState snapshot on fight night (Business+), or null
465
- * when nothing is being streamed. The WebSocket at wss://live.ufcalendar.com/v1
466
- * pushes the same document as it changes. */
483
+ /** Latest real-time LiveState snapshot on fight night (Pro plans and up), or
484
+ * null when nothing is being streamed. The WebSocket at
485
+ * wss://live.ufcalendar.com/v1 pushes the same document as it changes — see
486
+ * `subscribeLive()`. */
467
487
  eventLive(idOrSlug: string | number): Promise<LiveState | null>;
488
+ /**
489
+ * Subscribe to the live WebSocket and receive every frame (Pro plans and up).
490
+ *
491
+ * The UFC live API: opens `wss://live.ufcalendar.com/v1?key=…`, sends
492
+ * `{"action":"subscribe","event":<slug>}` and calls `onFrame` with each
493
+ * `LiveFrame` — round, running clock, unofficial in-fight statistics and the
494
+ * action timeline, the same `LiveState` `eventLive()` returns.
495
+ *
496
+ * ```ts
497
+ * const stop = api.subscribeLive('ufc-331', (f) => console.log(f.type), { until: 'final' });
498
+ * ```
499
+ *
500
+ * Uses the global `WebSocket` (browser, Node >= 22) unless you inject one
501
+ * (`ws` on older Node). A dropped socket reconnects and re-subscribes ONCE.
502
+ *
503
+ * @returns an unsubscribe function — call it to close the socket for good.
504
+ */
505
+ subscribeLive(event: string, onFrame: (frame: LiveFrame) => void, opts?: {
506
+ until?: 'final';
507
+ WebSocketImpl?: typeof WebSocket;
508
+ }): () => void;
468
509
  fight(fightId: number | string): Promise<Fight>;
469
510
  /** Per-fight totals for both corners. */
470
511
  fightStats(fightId: number | string): Promise<Record<string, unknown>[]>;
@@ -558,7 +599,7 @@ declare class FightAPI {
558
599
 
559
600
  /** Kept in lockstep with package.json and the User-Agent by
560
601
  * `apps/api/src/surfaces.test.ts`. Bump all three together. */
561
- declare const VERSION = "0.2.0";
602
+ declare const VERSION = "0.3.0";
562
603
 
563
604
  /**
564
605
  * This file was auto-generated by openapi-typescript.
@@ -992,8 +1033,8 @@ interface paths {
992
1033
  cookie?: never;
993
1034
  };
994
1035
  /**
995
- * Live snapshot (Business+)
996
- * @description The latest real-time document for an event on fight night — the same `LiveState` the WebSocket pushes: card order and statuses, the bout in progress (`current`: phase, round, running clock, unofficial totals and per-round stats, referee, a timestamped action timeline), and the last result. `data` is `null` with `meta.live: false` when nothing is being streamed. Poll it at most every few seconds, or connect to the WebSocket in `meta.websocket` (`?key=` on the URL, then send `{"action":"subscribe","event":"<slug>"}`) and receive every change as it lands. UFC only in v1. Requires Business or Enterprise (403 `tier_required` otherwise).
1036
+ * Live snapshot (Pro+)
1037
+ * @description The latest real-time document for an event on fight night — the same `LiveState` the WebSocket pushes: card order and statuses, the bout in progress (`current`: phase, round, running clock, unofficial totals and per-round stats, referee, a timestamped action timeline), and the last result. `data` is `null` with `meta.live: false` when nothing is being streamed. Poll it at most every few seconds, or connect to the WebSocket in `meta.websocket` (`?key=` on the URL, then send `{"action":"subscribe","event":"<slug>"}`) and receive every change as it lands. UFC only in v1. Requires Pro or higher (403 `tier_required` otherwise).
997
1038
  */
998
1039
  get: {
999
1040
  parameters: {
@@ -2538,7 +2579,7 @@ interface paths {
2538
2579
  };
2539
2580
  /**
2540
2581
  * Model win probabilities (UFC)
2541
- * @description UFCalendar model win probabilities for upcoming UFC bouts, refreshed every 30 minutes. Corner-guarded: a probability is only served while the stored pair matches the bout's current pair — a late opponent swap removes the row rather than mislabeling it. Not betting advice.
2582
+ * @description UFCalendar model win probabilities for upcoming UFC bouts, published about three weeks ahead of each card and repriced at least weekly. Corner-guarded: a probability is only served while the stored pair matches the bout's current pair — a late opponent swap removes the row and it returns repriced within 30 minutes, rather than mislabeling it. Not betting advice.
2542
2583
  */
2543
2584
  get: {
2544
2585
  parameters: {
@@ -3277,4 +3318,4 @@ interface paths {
3277
3318
  }
3278
3319
  type operations = Record<string, never>;
3279
3320
 
3280
- export { type Broadcast, type BroadcastRight, type CareerBout, type CareerStats, DEFAULT_BASE_URL, type Envelope, type EventChange, type EventDetail, type EventStatus, type EventSummary, type Fight, FightAPI, FightAPIError, type FightAPIOptions, type FightResult, type Fighter, type FighterImage, type FighterRef, type FighterSummary, type Judge, type LiveCardRow, type LiveCorner, type LiveCurrent, type LiveStatLine, type LiveState, type LiveTimelineEntry, type Meta, type Org, type Plan, type Plans, type PowerIndex, type Query, type QueryValue, type RankingDivision, type RankingEntry, type RankingsBoard, type RateLimit, type Scorecard, type ScorecardRound, type Scorecards, type Usage, VERSION, type Venue, type WebhookEndpoint, type operations, type paths };
3321
+ export { type Broadcast, type BroadcastRight, type CareerBout, type CareerStats, DEFAULT_BASE_URL, type Envelope, type EventChange, type EventDetail, type EventStatus, type EventSummary, type Fight, FightAPI, FightAPIError, type FightAPIOptions, type FightResult, type Fighter, type FighterImage, type FighterRef, type FighterSummary, type Judge, LIVE_WS_URL, type LiveCardRow, type LiveCorner, type LiveCurrent, type LiveFrame, type LiveStatLine, type LiveState, type LiveTimelineEntry, type Meta, type Org, type Plan, type Plans, type PowerIndex, type Query, type QueryValue, type RankingDivision, type RankingEntry, type RankingsBoard, type RateLimit, type Scorecard, type ScorecardRound, type Scorecards, type Usage, VERSION, type Venue, type WebhookEndpoint, type operations, type paths };
package/dist/index.d.ts CHANGED
@@ -183,6 +183,22 @@ interface LiveState {
183
183
  time: string | null;
184
184
  } | null;
185
185
  }
186
+ /** One frame off the live WebSocket (`wss://live.ufcalendar.com/v1`).
187
+ *
188
+ * `snapshot` arrives on subscribe (`data` is null with `reason: "not_live"`
189
+ * when nothing is streaming yet), `update` on every change, `fight.final`
190
+ * when a bout is official and `event.completed` when the card is over. */
191
+ interface LiveFrame {
192
+ type: 'snapshot' | 'update' | 'fight.final' | 'event.completed' | 'pong' | 'error';
193
+ data?: LiveState | null;
194
+ /** On a `snapshot` with no document: why (`not_live`). */
195
+ reason?: string;
196
+ /** On `error`. */
197
+ code?: string;
198
+ message?: string;
199
+ /** On `pong`. */
200
+ ts?: number;
201
+ }
186
202
  /** Identity fields only — the full record is on `/v1/fighters/{id}`. */
187
203
  interface FighterRef {
188
204
  id: number;
@@ -400,6 +416,9 @@ interface Usage {
400
416
  }
401
417
 
402
418
  declare const DEFAULT_BASE_URL = "https://api.ufcalendar.com/v1";
419
+ /** The live WebSocket (UFC fight nights, Pro plans and up) — the same document
420
+ * `GET /v1/events/{id}/live` returns, pushed on every change. */
421
+ declare const LIVE_WS_URL = "wss://live.ufcalendar.com/v1";
403
422
  interface FightAPIOptions {
404
423
  /** `ufcalendar_…` key from https://www.ufcalendar.com/account/api. Falls back
405
424
  * to `UFCALENDAR_API_KEY` / `UFCAL_API_KEY` in the environment. Only
@@ -461,10 +480,32 @@ declare class FightAPI {
461
480
  event(idOrSlug: string | number): Promise<EventDetail>;
462
481
  /** Card-change diff log (fight added/removed, opponent swapped, date moved, profile merged). */
463
482
  eventChanges(idOrSlug: string | number): Promise<EventChange[]>;
464
- /** Latest real-time LiveState snapshot on fight night (Business+), or null
465
- * when nothing is being streamed. The WebSocket at wss://live.ufcalendar.com/v1
466
- * pushes the same document as it changes. */
483
+ /** Latest real-time LiveState snapshot on fight night (Pro plans and up), or
484
+ * null when nothing is being streamed. The WebSocket at
485
+ * wss://live.ufcalendar.com/v1 pushes the same document as it changes — see
486
+ * `subscribeLive()`. */
467
487
  eventLive(idOrSlug: string | number): Promise<LiveState | null>;
488
+ /**
489
+ * Subscribe to the live WebSocket and receive every frame (Pro plans and up).
490
+ *
491
+ * The UFC live API: opens `wss://live.ufcalendar.com/v1?key=…`, sends
492
+ * `{"action":"subscribe","event":<slug>}` and calls `onFrame` with each
493
+ * `LiveFrame` — round, running clock, unofficial in-fight statistics and the
494
+ * action timeline, the same `LiveState` `eventLive()` returns.
495
+ *
496
+ * ```ts
497
+ * const stop = api.subscribeLive('ufc-331', (f) => console.log(f.type), { until: 'final' });
498
+ * ```
499
+ *
500
+ * Uses the global `WebSocket` (browser, Node >= 22) unless you inject one
501
+ * (`ws` on older Node). A dropped socket reconnects and re-subscribes ONCE.
502
+ *
503
+ * @returns an unsubscribe function — call it to close the socket for good.
504
+ */
505
+ subscribeLive(event: string, onFrame: (frame: LiveFrame) => void, opts?: {
506
+ until?: 'final';
507
+ WebSocketImpl?: typeof WebSocket;
508
+ }): () => void;
468
509
  fight(fightId: number | string): Promise<Fight>;
469
510
  /** Per-fight totals for both corners. */
470
511
  fightStats(fightId: number | string): Promise<Record<string, unknown>[]>;
@@ -558,7 +599,7 @@ declare class FightAPI {
558
599
 
559
600
  /** Kept in lockstep with package.json and the User-Agent by
560
601
  * `apps/api/src/surfaces.test.ts`. Bump all three together. */
561
- declare const VERSION = "0.2.0";
602
+ declare const VERSION = "0.3.0";
562
603
 
563
604
  /**
564
605
  * This file was auto-generated by openapi-typescript.
@@ -992,8 +1033,8 @@ interface paths {
992
1033
  cookie?: never;
993
1034
  };
994
1035
  /**
995
- * Live snapshot (Business+)
996
- * @description The latest real-time document for an event on fight night — the same `LiveState` the WebSocket pushes: card order and statuses, the bout in progress (`current`: phase, round, running clock, unofficial totals and per-round stats, referee, a timestamped action timeline), and the last result. `data` is `null` with `meta.live: false` when nothing is being streamed. Poll it at most every few seconds, or connect to the WebSocket in `meta.websocket` (`?key=` on the URL, then send `{"action":"subscribe","event":"<slug>"}`) and receive every change as it lands. UFC only in v1. Requires Business or Enterprise (403 `tier_required` otherwise).
1036
+ * Live snapshot (Pro+)
1037
+ * @description The latest real-time document for an event on fight night — the same `LiveState` the WebSocket pushes: card order and statuses, the bout in progress (`current`: phase, round, running clock, unofficial totals and per-round stats, referee, a timestamped action timeline), and the last result. `data` is `null` with `meta.live: false` when nothing is being streamed. Poll it at most every few seconds, or connect to the WebSocket in `meta.websocket` (`?key=` on the URL, then send `{"action":"subscribe","event":"<slug>"}`) and receive every change as it lands. UFC only in v1. Requires Pro or higher (403 `tier_required` otherwise).
997
1038
  */
998
1039
  get: {
999
1040
  parameters: {
@@ -2538,7 +2579,7 @@ interface paths {
2538
2579
  };
2539
2580
  /**
2540
2581
  * Model win probabilities (UFC)
2541
- * @description UFCalendar model win probabilities for upcoming UFC bouts, refreshed every 30 minutes. Corner-guarded: a probability is only served while the stored pair matches the bout's current pair — a late opponent swap removes the row rather than mislabeling it. Not betting advice.
2582
+ * @description UFCalendar model win probabilities for upcoming UFC bouts, published about three weeks ahead of each card and repriced at least weekly. Corner-guarded: a probability is only served while the stored pair matches the bout's current pair — a late opponent swap removes the row and it returns repriced within 30 minutes, rather than mislabeling it. Not betting advice.
2542
2583
  */
2543
2584
  get: {
2544
2585
  parameters: {
@@ -3277,4 +3318,4 @@ interface paths {
3277
3318
  }
3278
3319
  type operations = Record<string, never>;
3279
3320
 
3280
- export { type Broadcast, type BroadcastRight, type CareerBout, type CareerStats, DEFAULT_BASE_URL, type Envelope, type EventChange, type EventDetail, type EventStatus, type EventSummary, type Fight, FightAPI, FightAPIError, type FightAPIOptions, type FightResult, type Fighter, type FighterImage, type FighterRef, type FighterSummary, type Judge, type LiveCardRow, type LiveCorner, type LiveCurrent, type LiveStatLine, type LiveState, type LiveTimelineEntry, type Meta, type Org, type Plan, type Plans, type PowerIndex, type Query, type QueryValue, type RankingDivision, type RankingEntry, type RankingsBoard, type RateLimit, type Scorecard, type ScorecardRound, type Scorecards, type Usage, VERSION, type Venue, type WebhookEndpoint, type operations, type paths };
3321
+ export { type Broadcast, type BroadcastRight, type CareerBout, type CareerStats, DEFAULT_BASE_URL, type Envelope, type EventChange, type EventDetail, type EventStatus, type EventSummary, type Fight, FightAPI, FightAPIError, type FightAPIOptions, type FightResult, type Fighter, type FighterImage, type FighterRef, type FighterSummary, type Judge, LIVE_WS_URL, type LiveCardRow, type LiveCorner, type LiveCurrent, type LiveFrame, type LiveStatLine, type LiveState, type LiveTimelineEntry, type Meta, type Org, type Plan, type Plans, type PowerIndex, type Query, type QueryValue, type RankingDivision, type RankingEntry, type RankingsBoard, type RateLimit, type Scorecard, type ScorecardRound, type Scorecards, type Usage, VERSION, type Venue, type WebhookEndpoint, type operations, type paths };
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  // src/version.ts
2
- var VERSION = "0.2.0";
2
+ var VERSION = "0.3.0";
3
3
 
4
4
  // src/client.ts
5
5
  var DEFAULT_BASE_URL = "https://api.ufcalendar.com/v1";
6
+ var LIVE_WS_URL = "wss://live.ufcalendar.com/v1";
6
7
  var DEFAULT_TIMEOUT_MS = 3e4;
7
8
  var FightAPIError = class extends Error {
8
9
  status;
@@ -170,12 +171,87 @@ var FightAPI = class {
170
171
  eventChanges(idOrSlug) {
171
172
  return this.get(`events/${encodeURIComponent(String(idOrSlug))}/changes`);
172
173
  }
173
- /** Latest real-time LiveState snapshot on fight night (Business+), or null
174
- * when nothing is being streamed. The WebSocket at wss://live.ufcalendar.com/v1
175
- * pushes the same document as it changes. */
174
+ /** Latest real-time LiveState snapshot on fight night (Pro plans and up), or
175
+ * null when nothing is being streamed. The WebSocket at
176
+ * wss://live.ufcalendar.com/v1 pushes the same document as it changes — see
177
+ * `subscribeLive()`. */
176
178
  eventLive(idOrSlug) {
177
179
  return this.get(`events/${encodeURIComponent(String(idOrSlug))}/live`);
178
180
  }
181
+ /**
182
+ * Subscribe to the live WebSocket and receive every frame (Pro plans and up).
183
+ *
184
+ * The UFC live API: opens `wss://live.ufcalendar.com/v1?key=…`, sends
185
+ * `{"action":"subscribe","event":<slug>}` and calls `onFrame` with each
186
+ * `LiveFrame` — round, running clock, unofficial in-fight statistics and the
187
+ * action timeline, the same `LiveState` `eventLive()` returns.
188
+ *
189
+ * ```ts
190
+ * const stop = api.subscribeLive('ufc-331', (f) => console.log(f.type), { until: 'final' });
191
+ * ```
192
+ *
193
+ * Uses the global `WebSocket` (browser, Node >= 22) unless you inject one
194
+ * (`ws` on older Node). A dropped socket reconnects and re-subscribes ONCE.
195
+ *
196
+ * @returns an unsubscribe function — call it to close the socket for good.
197
+ */
198
+ subscribeLive(event, onFrame, opts = {}) {
199
+ const Impl = opts.WebSocketImpl ?? globalThis.WebSocket;
200
+ if (!Impl) {
201
+ throw new Error(
202
+ "subscribeLive() needs a WebSocket: use Node >= 22 (or a browser), or pass { WebSocketImpl } \u2014 e.g. `import WebSocket from 'ws'`."
203
+ );
204
+ }
205
+ if (!this.apiKey) {
206
+ throw new Error(
207
+ "No API key. The live stream authenticates with ?key=. Keys (and the free 1-day trial) live at https://www.ufcalendar.com/account/api"
208
+ );
209
+ }
210
+ const url = `${LIVE_WS_URL}?key=${encodeURIComponent(this.apiKey)}`;
211
+ let done = false;
212
+ let reconnects = 0;
213
+ let sock = null;
214
+ const open = () => {
215
+ const ws = new Impl(url);
216
+ sock = ws;
217
+ ws.onopen = () => {
218
+ try {
219
+ ws.send(JSON.stringify({ action: "subscribe", event }));
220
+ } catch {
221
+ }
222
+ };
223
+ ws.onmessage = (ev) => {
224
+ let frame;
225
+ try {
226
+ frame = JSON.parse(typeof ev.data === "string" ? ev.data : String(ev.data));
227
+ } catch {
228
+ return;
229
+ }
230
+ if (!frame || typeof frame !== "object") return;
231
+ onFrame(frame);
232
+ if (opts.until === "final" && frame.type === "fight.final") {
233
+ done = true;
234
+ try {
235
+ ws.close();
236
+ } catch {
237
+ }
238
+ }
239
+ };
240
+ ws.onclose = () => {
241
+ if (done || reconnects >= 1) return;
242
+ reconnects += 1;
243
+ open();
244
+ };
245
+ };
246
+ open();
247
+ return () => {
248
+ done = true;
249
+ try {
250
+ sock?.close();
251
+ } catch {
252
+ }
253
+ };
254
+ }
179
255
  /* -------------------------------------------------------------- fights */
180
256
  fight(fightId) {
181
257
  return this.get(`fights/${encodeURIComponent(String(fightId))}`);
@@ -334,6 +410,7 @@ export {
334
410
  DEFAULT_BASE_URL,
335
411
  FightAPI,
336
412
  FightAPIError,
413
+ LIVE_WS_URL,
337
414
  VERSION
338
415
  };
339
416
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts","../src/client.ts"],"sourcesContent":["/** Kept in lockstep with package.json and the User-Agent by\n * `apps/api/src/surfaces.test.ts`. Bump all three together. */\nexport const VERSION = '0.2.0';\n","/**\n * Thin, dependency-free client for https://api.ufcalendar.com/v1.\n *\n * Every method returns the parsed `data` value of the JSON envelope\n * (`{\"data\": ..., \"meta\": ...}`); list endpoints are async generators that\n * follow cursor pagination for you. Errors throw `FightAPIError` carrying\n * the API's `code`, `message` and `requestId` — quote the request id when\n * you write to api@ufcalendar.com.\n *\n * Mirrors sdk/python/ufcalendar/client.py one method per endpoint, in\n * camelCase. The two clients are kept in lockstep by\n * `apps/api/src/surfaces.test.ts`.\n *\n * The API serves no betting odds, by design. Fighter `images` are Wikimedia\n * Commons / Creative Commons files — the `license` and `artist` fields you\n * receive must be displayed as a credit.\n */\nimport { VERSION } from './version';\nimport type {\n BroadcastRight,\n CareerBout,\n CareerStats,\n Envelope,\n EventChange,\n EventDetail,\n EventSummary,\n Fight,\n Fighter,\n FighterSummary,\n Judge,\n Meta,\n Org,\n Plans,\n PowerIndex,\n RankingsBoard,\n RateLimit,\n Scorecards,\n Usage,\n Venue,\n WebhookEndpoint,\n LiveState,\n} from './types';\n\nexport const DEFAULT_BASE_URL = 'https://api.ufcalendar.com/v1';\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport interface FightAPIOptions {\n /** `ufcalendar_…` key from https://www.ufcalendar.com/account/api. Falls back\n * to `UFCALENDAR_API_KEY` / `UFCAL_API_KEY` in the environment. Only\n * `plans()` works without one. */\n apiKey?: string;\n /** Override for testing; defaults to the production `/v1`. */\n baseUrl?: string;\n /** Inject a fetch (a test double, an instrumented fetch, undici). */\n fetch?: typeof fetch;\n /** Per-request timeout. Default 30s. */\n timeoutMs?: number;\n /** Extra headers on every request. */\n headers?: Record<string, string>;\n}\n\n/** An error response from the Fight API. */\nexport class FightAPIError extends Error {\n readonly status: number;\n readonly code: string;\n readonly requestId: string | null;\n\n constructor(status: number, code: string, message: string, requestId: string | null = null) {\n super(`${status} ${code}: ${message}${requestId ? ` (request_id=${requestId})` : ''}`);\n this.name = 'FightAPIError';\n this.status = status;\n this.code = code;\n this.requestId = requestId;\n }\n}\n\nexport type QueryValue = string | number | boolean | null | undefined;\nexport type Query = Record<string, QueryValue>;\n\ninterface PageOptions {\n /** Stop after this many rows (across pages). */\n limit?: number;\n}\n\n/**\n * A browser forbids setting User-Agent on fetch, so we only set ours\n * off-browser. The test is `document`, NOT `navigator`: Node 21+ ships a\n * global `navigator`, so a navigator check would silently drop the header\n * on every modern Node runtime — which is the one place it works.\n */\nfunction canSetUserAgent(): boolean {\n return typeof document === 'undefined' && typeof window === 'undefined';\n}\n\nfunction envKey(): string | undefined {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;\n return env?.UFCALENDAR_API_KEY || env?.UFCAL_API_KEY;\n}\n\nexport class FightAPI {\n readonly baseUrl: string;\n readonly apiKey: string | undefined;\n\n /** `meta` of the most recent response (pagination cursor, generated_at …). */\n lastMeta: Meta | null = null;\n /** `X-RateLimit-*` of the most recent response. */\n lastRateLimit: RateLimit = { limit: null, remaining: null, reset: null };\n\n readonly #fetch: typeof fetch;\n readonly #timeoutMs: number;\n readonly #headers: Record<string, string>;\n\n constructor(apiKeyOrOptions?: string | FightAPIOptions, options: FightAPIOptions = {}) {\n const opts: FightAPIOptions =\n typeof apiKeyOrOptions === 'string'\n ? { ...options, apiKey: apiKeyOrOptions }\n : { ...options, ...(apiKeyOrOptions ?? {}) };\n this.apiKey = opts.apiKey ?? envKey();\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n this.#fetch = opts.fetch ?? globalThis.fetch;\n this.#timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.#headers = opts.headers ?? {};\n if (!this.#fetch) {\n throw new Error('No global fetch available. Use Node 18+ or pass { fetch }.');\n }\n }\n\n /* ---------------------------------------------------------------- core */\n\n #url(path: string, params?: Query): string {\n const url = new URL(`${this.baseUrl}/${path.replace(/^\\/+/, '')}`);\n for (const [k, v] of Object.entries(params ?? {})) {\n if (v === undefined || v === null) continue;\n url.searchParams.set(k, String(v));\n }\n return url.toString();\n }\n\n async #request<T>(\n method: string,\n path: string,\n { params, body, open = false }: { params?: Query; body?: unknown; open?: boolean } = {},\n ): Promise<Envelope<T>> {\n if (!this.apiKey && !open) {\n throw new FightAPIError(\n 401,\n 'no_api_key',\n 'No API key. Pass new FightAPI(\"ufcalendar_...\") or set UFCALENDAR_API_KEY. ' +\n 'Keys (and the free 1-day trial) live at https://www.ufcalendar.com/account/api',\n );\n }\n const headers: Record<string, string> = { Accept: 'application/json', ...this.#headers };\n if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;\n if (canSetUserAgent()) headers['User-Agent'] = `ufcalendar-typescript/${VERSION}`;\n if (body !== undefined) headers['Content-Type'] = 'application/json';\n\n let res: Response;\n try {\n res = await this.#fetch(this.#url(path, params), {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: AbortSignal.timeout(this.#timeoutMs),\n });\n } catch (e) {\n const err = e as { name?: string; message?: string };\n if (err?.name === 'TimeoutError') {\n throw new FightAPIError(0, 'timeout', `Request to ${path} timed out after ${this.#timeoutMs}ms`);\n }\n throw new FightAPIError(0, 'network_error', err?.message ?? String(e));\n }\n\n this.lastRateLimit = {\n limit: res.headers.get('X-RateLimit-Limit'),\n remaining: res.headers.get('X-RateLimit-Remaining'),\n reset: res.headers.get('X-RateLimit-Reset'),\n };\n\n if (res.status === 204) {\n this.lastMeta = null;\n return { data: undefined as T };\n }\n\n const text = await res.text();\n let parsed: unknown = undefined;\n try {\n parsed = text ? JSON.parse(text) : undefined;\n } catch {\n parsed = undefined;\n }\n\n if (!res.ok) {\n const err = (parsed as { error?: { code?: string; message?: string; request_id?: string } } | undefined)?.error;\n if (err) {\n throw new FightAPIError(\n res.status,\n String(err.code ?? 'error'),\n String(err.message ?? text.slice(0, 200)),\n err.request_id ?? res.headers.get('x-request-id'),\n );\n }\n throw new FightAPIError(res.status, 'http_error', text.slice(0, 200), res.headers.get('x-request-id'));\n }\n\n const env = (parsed ?? {}) as Envelope<T>;\n this.lastMeta = env.meta ?? null;\n return env;\n }\n\n /** Raw GET returning the `data` value. Escape hatch for new endpoints. */\n async get<T = unknown>(path: string, params?: Query): Promise<T> {\n return (await this.#request<T>('GET', path, { params })).data;\n }\n\n /** Raw GET returning the whole `{data, meta}` envelope. */\n async getWithMeta<T = unknown>(path: string, params?: Query): Promise<Envelope<T>> {\n return this.#request<T>('GET', path, { params });\n }\n\n async *#paginate<T>(path: string, params: Query, opts: PageOptions = {}): AsyncGenerator<T, void, void> {\n // Never fetch a bigger page than the caller wants rows: `limit: 3` used\n // to pull 100 rows and throw 97 away, burning the caller's quota on a\n // request they did not ask for (and, on a trial key, a noticeable slice\n // of the 100 they get).\n const pageSize = Math.min(100, opts.limit ?? 100);\n const query: Query = { limit: pageSize, ...params };\n let seen = 0;\n for (;;) {\n const env = await this.#request<T[]>('GET', path, { params: query });\n for (const row of env.data ?? []) {\n yield row;\n seen += 1;\n if (opts.limit !== undefined && seen >= opts.limit) return;\n }\n const cursor = env.meta?.pagination?.next_cursor;\n if (!cursor) return;\n query.cursor = cursor;\n }\n }\n\n /* --------------------------------------------------------------- plans */\n\n /**\n * Plans, quotas, the free 1-day trial rule, the MCP endpoint and the doc\n * links. The only endpoint that answers without a credential.\n */\n async plans(): Promise<Plans> {\n return (await this.#request<Plans>('GET', 'plans', { open: true })).data;\n }\n\n /* ---------------------------------------------------------------- orgs */\n\n /** Launch orgs with capability flags (stats / rounds / rankings / broadcasts / predictions). */\n orgs(): Promise<Org[]> {\n return this.get<Org[]>('orgs');\n }\n\n org(slug: string): Promise<Org> {\n return this.get<Org>(`orgs/${encodeURIComponent(slug)}`);\n }\n\n /* -------------------------------------------------------------- events */\n\n /**\n * Schedule + results. Bare call = the upcoming calendar, soonest first.\n * `status: 'completed'` (or `order: 'desc'`) browses the archive\n * newest-first. `from` / `to` are `YYYY-MM-DD`.\n */\n events(\n opts: {\n org?: string;\n status?: string;\n from?: string;\n to?: string;\n order?: 'asc' | 'desc';\n limit?: number;\n } = {},\n ): AsyncGenerator<EventSummary, void, void> {\n const { limit, ...params } = opts;\n return this.#paginate<EventSummary>('events', params as Query, { limit });\n }\n\n /** One event with its full fight card, venue and broadcasts. */\n event(idOrSlug: string | number): Promise<EventDetail> {\n return this.get<EventDetail>(`events/${encodeURIComponent(String(idOrSlug))}`);\n }\n\n /** Card-change diff log (fight added/removed, opponent swapped, date moved, profile merged). */\n eventChanges(idOrSlug: string | number): Promise<EventChange[]> {\n return this.get<EventChange[]>(`events/${encodeURIComponent(String(idOrSlug))}/changes`);\n }\n\n /** Latest real-time LiveState snapshot on fight night (Business+), or null\n * when nothing is being streamed. The WebSocket at wss://live.ufcalendar.com/v1\n * pushes the same document as it changes. */\n eventLive(idOrSlug: string | number): Promise<LiveState | null> {\n return this.get<LiveState | null>(`events/${encodeURIComponent(String(idOrSlug))}/live`);\n }\n\n /* -------------------------------------------------------------- fights */\n\n fight(fightId: number | string): Promise<Fight> {\n return this.get<Fight>(`fights/${encodeURIComponent(String(fightId))}`);\n }\n\n /** Per-fight totals for both corners. */\n fightStats(fightId: number | string): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>(`fights/${encodeURIComponent(String(fightId))}/stats`);\n }\n\n /** Round-by-round stat lines for both corners. */\n fightRounds(fightId: number | string): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>(`fights/${encodeURIComponent(String(fightId))}/rounds`);\n }\n\n /**\n * The judges' scorecards for a bout — the official commission record:\n * decision type, point deductions, and one card per judge with every\n * round, the totals and `winner_fighter_id`. Scores are oriented to\n * `fighter_a_id` / `fighter_b_id`, both repeated on the payload.\n *\n * Check `scores_known` on a card before charting totals: when it is false\n * the commission published only the outcome. Throws 404 for a bout that\n * did not go to the judges.\n */\n fightScorecards(fightId: number | string): Promise<Scorecards> {\n return this.get<Scorecards>(`fights/${encodeURIComponent(String(fightId))}/scorecards`);\n }\n\n /* -------------------------------------------------------------- judges */\n\n /**\n * Every official who has scored a launch-org bout, busiest first. Rates\n * mean little below ~10 fights; pass `minFights: 10`.\n */\n judges(\n opts: { q?: string; org?: string; minFights?: number; limit?: number } = {},\n ): AsyncGenerator<Judge, void, void> {\n const { limit, minFights, ...rest } = opts;\n return this.#paginate<Judge>('judges', { ...rest, min_fights: minFights } as Query, { limit });\n }\n\n judge(judgeId: number | string): Promise<Judge> {\n return this.get<Judge>(`judges/${encodeURIComponent(String(judgeId))}`);\n }\n\n /** Every card this judge has turned in, newest first. */\n judgeScorecards(\n judgeId: number | string,\n opts: { limit?: number } = {},\n ): AsyncGenerator<Record<string, unknown>, void, void> {\n return this.#paginate<Record<string, unknown>>(\n `judges/${encodeURIComponent(String(judgeId))}/scorecards`,\n {},\n opts,\n );\n }\n\n /* ------------------------------------------------------------ fighters */\n\n fighters(\n opts: { q?: string; org?: string; country?: string; limit?: number } = {},\n ): AsyncGenerator<FighterSummary, void, void> {\n const { limit, ...params } = opts;\n return this.#paginate<FighterSummary>('fighters', params as Query, { limit });\n }\n\n /** Bio, records, career stats, Power Index and CC-licensed images. */\n fighter(idOrSlug: string | number): Promise<Fighter> {\n return this.get<Fighter>(`fighters/${encodeURIComponent(String(idOrSlug))}`);\n }\n\n /** Complete multi-promotion career timeline. */\n fighterHistory(idOrSlug: string | number): Promise<CareerBout[]> {\n return this.get<CareerBout[]>(`fighters/${encodeURIComponent(String(idOrSlug))}/history`);\n }\n\n /** Career statistics, one source-stamped row per scope (`pro-mma`, `ufc-only` …). */\n fighterStats(idOrSlug: string | number): Promise<CareerStats[]> {\n return this.get<CareerStats[]>(`fighters/${encodeURIComponent(String(idOrSlug))}/stats`);\n }\n\n /** Every official ranking row the fighter ever held, newest first. */\n fighterRankings(idOrSlug: string | number): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>(`fighters/${encodeURIComponent(String(idOrSlug))}/rankings`);\n }\n\n fighterPowerIndex(idOrSlug: string | number): Promise<PowerIndex & Record<string, unknown>> {\n return this.get<PowerIndex & Record<string, unknown>>(\n `fighters/${encodeURIComponent(String(idOrSlug))}/power-index`,\n );\n }\n\n /* ------------------------------------------------------------ rankings */\n\n /**\n * Official board, point-in-time. `date: 'YYYY-MM-DD'` returns the board\n * that was valid on that day (UFC history back to 2013; rank 0 = champion).\n */\n rankings(org = 'ufc', opts: { date?: string; board?: string } = {}): Promise<RankingsBoard> {\n return this.get<RankingsBoard>(`rankings/${encodeURIComponent(org)}`, opts as Query);\n }\n\n divisionRankings(org: string, division: string, opts: { date?: string } = {}): Promise<RankingsBoard> {\n return this.get<RankingsBoard>(\n `rankings/${encodeURIComponent(org)}/${encodeURIComponent(division)}`,\n opts as Query,\n );\n }\n\n /** Current champions across every launch org. */\n champions(): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>('champions');\n }\n\n /** UFCalendar Power Index board. */\n powerIndex(org = 'ufc'): Promise<Record<string, unknown>> {\n return this.get<Record<string, unknown>>(`power-index/${encodeURIComponent(org)}`);\n }\n\n /* ---------------------------------------------------------------- misc */\n\n /** Model win probabilities for upcoming UFC bouts. */\n predictionsUpcoming(): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>('predictions/upcoming');\n }\n\n /** Who airs the promotion, per ISO-2 country. */\n broadcastRights(org = 'ufc', opts: { country?: string } = {}): Promise<BroadcastRight[]> {\n return this.get<BroadcastRight[]>(`broadcast-rights/${encodeURIComponent(org)}`, opts as Query);\n }\n\n venue(venueId: number | string): Promise<Venue> {\n return this.get<Venue>(`venues/${encodeURIComponent(String(venueId))}`);\n }\n\n /** Typeahead across fighters and events. */\n search(q: string): Promise<Record<string, unknown>> {\n return this.get<Record<string, unknown>>('search', { q });\n }\n\n /** Your key's month-to-date quota usage. */\n usage(): Promise<Usage> {\n return this.get<Usage>('usage');\n }\n\n /**\n * Subscribable ICS feed URL for calendar apps (authenticates via `?key=`).\n *\n * Throws without a key rather than returning `?key=`: that empty URL is\n * pasted into a calendar app, fails there hours later, and the failure\n * surfaces nowhere near this call.\n */\n calendarIcsUrl(org = 'ufc'): string {\n if (!this.apiKey) {\n throw new FightAPIError(\n 401,\n 'no_api_key',\n 'No API key. Pass new FightAPI(\"ufcalendar_...\") or set UFCALENDAR_API_KEY. ' +\n 'Keys (and the free 1-day trial) live at https://www.ufcalendar.com/account/api',\n );\n }\n return `${this.baseUrl}/calendar/${encodeURIComponent(org)}.ics?key=${encodeURIComponent(this.apiKey)}`;\n }\n\n /* ------------------------------------------------------------ webhooks */\n\n webhookEndpoints(): Promise<WebhookEndpoint[]> {\n return this.get<WebhookEndpoint[]>('webhook-endpoints');\n }\n\n /**\n * Register a signed webhook (Pro and up). `events` ⊆ `event.announced`,\n * `fight.result`, `card.changed`, `event.completed`. The signing secret is\n * returned ONCE, in this response.\n */\n async createWebhookEndpoint(url: string, events?: string[]): Promise<WebhookEndpoint> {\n const body: Record<string, unknown> = { url };\n if (events?.length) body.events = events;\n return (await this.#request<WebhookEndpoint>('POST', 'webhook-endpoints', { body })).data;\n }\n\n async deleteWebhookEndpoint(endpointId: number | string): Promise<void> {\n await this.#request<unknown>('DELETE', `webhook-endpoints/${encodeURIComponent(String(endpointId))}`);\n }\n\n async rotateWebhookSecret(endpointId: number | string): Promise<WebhookEndpoint> {\n return (\n await this.#request<WebhookEndpoint>(\n 'POST',\n `webhook-endpoints/${encodeURIComponent(String(endpointId))}/rotate-secret`,\n )\n ).data;\n }\n}\n"],"mappings":";AAEO,IAAM,UAAU;;;ACyChB,IAAM,mBAAmB;AAChC,IAAM,qBAAqB;AAkBpB,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAAgB,MAAc,SAAiB,YAA2B,MAAM;AAC1F,UAAM,GAAG,MAAM,IAAI,IAAI,KAAK,OAAO,GAAG,YAAY,gBAAgB,SAAS,MAAM,EAAE,EAAE;AACrF,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AACZ,SAAK,YAAY;AAAA,EACnB;AACF;AAgBA,SAAS,kBAA2B;AAClC,SAAO,OAAO,aAAa,eAAe,OAAO,WAAW;AAC9D;AAEA,SAAS,SAA6B;AACpC,QAAM,MAAO,WAA0E,SAAS;AAChG,SAAO,KAAK,sBAAsB,KAAK;AACzC;AAEO,IAAM,WAAN,MAAe;AAAA,EACX;AAAA,EACA;AAAA;AAAA,EAGT,WAAwB;AAAA;AAAA,EAExB,gBAA2B,EAAE,OAAO,MAAM,WAAW,MAAM,OAAO,KAAK;AAAA,EAE9D;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,iBAA4C,UAA2B,CAAC,GAAG;AACrF,UAAM,OACJ,OAAO,oBAAoB,WACvB,EAAE,GAAG,SAAS,QAAQ,gBAAgB,IACtC,EAAE,GAAG,SAAS,GAAI,mBAAmB,CAAC,EAAG;AAC/C,SAAK,SAAS,KAAK,UAAU,OAAO;AACpC,SAAK,WAAW,KAAK,WAAW,kBAAkB,QAAQ,QAAQ,EAAE;AACpE,SAAK,SAAS,KAAK,SAAS,WAAW;AACvC,SAAK,aAAa,KAAK,aAAa;AACpC,SAAK,WAAW,KAAK,WAAW,CAAC;AACjC,QAAI,CAAC,KAAK,QAAQ;AAChB,YAAM,IAAI,MAAM,4DAA4D;AAAA,IAC9E;AAAA,EACF;AAAA;AAAA,EAIA,KAAK,MAAc,QAAwB;AACzC,UAAM,MAAM,IAAI,IAAI,GAAG,KAAK,OAAO,IAAI,KAAK,QAAQ,QAAQ,EAAE,CAAC,EAAE;AACjE,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,UAAU,CAAC,CAAC,GAAG;AACjD,UAAI,MAAM,UAAa,MAAM,KAAM;AACnC,UAAI,aAAa,IAAI,GAAG,OAAO,CAAC,CAAC;AAAA,IACnC;AACA,WAAO,IAAI,SAAS;AAAA,EACtB;AAAA,EAEA,MAAM,SACJ,QACA,MACA,EAAE,QAAQ,MAAM,OAAO,MAAM,IAAwD,CAAC,GAChE;AACtB,QAAI,CAAC,KAAK,UAAU,CAAC,MAAM;AACzB,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MAEF;AAAA,IACF;AACA,UAAM,UAAkC,EAAE,QAAQ,oBAAoB,GAAG,KAAK,SAAS;AACvF,QAAI,KAAK,OAAQ,SAAQ,gBAAgB,UAAU,KAAK,MAAM;AAC9D,QAAI,gBAAgB,EAAG,SAAQ,YAAY,IAAI,yBAAyB,OAAO;AAC/E,QAAI,SAAS,OAAW,SAAQ,cAAc,IAAI;AAElD,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,KAAK,OAAO,KAAK,KAAK,MAAM,MAAM,GAAG;AAAA,QAC/C;AAAA,QACA;AAAA,QACA,MAAM,SAAS,SAAY,SAAY,KAAK,UAAU,IAAI;AAAA,QAC1D,QAAQ,YAAY,QAAQ,KAAK,UAAU;AAAA,MAC7C,CAAC;AAAA,IACH,SAAS,GAAG;AACV,YAAM,MAAM;AACZ,UAAI,KAAK,SAAS,gBAAgB;AAChC,cAAM,IAAI,cAAc,GAAG,WAAW,cAAc,IAAI,oBAAoB,KAAK,UAAU,IAAI;AAAA,MACjG;AACA,YAAM,IAAI,cAAc,GAAG,iBAAiB,KAAK,WAAW,OAAO,CAAC,CAAC;AAAA,IACvE;AAEA,SAAK,gBAAgB;AAAA,MACnB,OAAO,IAAI,QAAQ,IAAI,mBAAmB;AAAA,MAC1C,WAAW,IAAI,QAAQ,IAAI,uBAAuB;AAAA,MAClD,OAAO,IAAI,QAAQ,IAAI,mBAAmB;AAAA,IAC5C;AAEA,QAAI,IAAI,WAAW,KAAK;AACtB,WAAK,WAAW;AAChB,aAAO,EAAE,MAAM,OAAe;AAAA,IAChC;AAEA,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,QAAI,SAAkB;AACtB,QAAI;AACF,eAAS,OAAO,KAAK,MAAM,IAAI,IAAI;AAAA,IACrC,QAAQ;AACN,eAAS;AAAA,IACX;AAEA,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,MAAO,QAA6F;AAC1G,UAAI,KAAK;AACP,cAAM,IAAI;AAAA,UACR,IAAI;AAAA,UACJ,OAAO,IAAI,QAAQ,OAAO;AAAA,UAC1B,OAAO,IAAI,WAAW,KAAK,MAAM,GAAG,GAAG,CAAC;AAAA,UACxC,IAAI,cAAc,IAAI,QAAQ,IAAI,cAAc;AAAA,QAClD;AAAA,MACF;AACA,YAAM,IAAI,cAAc,IAAI,QAAQ,cAAc,KAAK,MAAM,GAAG,GAAG,GAAG,IAAI,QAAQ,IAAI,cAAc,CAAC;AAAA,IACvG;AAEA,UAAM,MAAO,UAAU,CAAC;AACxB,SAAK,WAAW,IAAI,QAAQ;AAC5B,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,IAAiB,MAAc,QAA4B;AAC/D,YAAQ,MAAM,KAAK,SAAY,OAAO,MAAM,EAAE,OAAO,CAAC,GAAG;AAAA,EAC3D;AAAA;AAAA,EAGA,MAAM,YAAyB,MAAc,QAAsC;AACjF,WAAO,KAAK,SAAY,OAAO,MAAM,EAAE,OAAO,CAAC;AAAA,EACjD;AAAA,EAEA,OAAO,UAAa,MAAc,QAAe,OAAoB,CAAC,GAAkC;AAKtG,UAAM,WAAW,KAAK,IAAI,KAAK,KAAK,SAAS,GAAG;AAChD,UAAM,QAAe,EAAE,OAAO,UAAU,GAAG,OAAO;AAClD,QAAI,OAAO;AACX,eAAS;AACP,YAAM,MAAM,MAAM,KAAK,SAAc,OAAO,MAAM,EAAE,QAAQ,MAAM,CAAC;AACnE,iBAAW,OAAO,IAAI,QAAQ,CAAC,GAAG;AAChC,cAAM;AACN,gBAAQ;AACR,YAAI,KAAK,UAAU,UAAa,QAAQ,KAAK,MAAO;AAAA,MACtD;AACA,YAAM,SAAS,IAAI,MAAM,YAAY;AACrC,UAAI,CAAC,OAAQ;AACb,YAAM,SAAS;AAAA,IACjB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,QAAwB;AAC5B,YAAQ,MAAM,KAAK,SAAgB,OAAO,SAAS,EAAE,MAAM,KAAK,CAAC,GAAG;AAAA,EACtE;AAAA;AAAA;AAAA,EAKA,OAAuB;AACrB,WAAO,KAAK,IAAW,MAAM;AAAA,EAC/B;AAAA,EAEA,IAAI,MAA4B;AAC9B,WAAO,KAAK,IAAS,QAAQ,mBAAmB,IAAI,CAAC,EAAE;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,OACE,OAOI,CAAC,GACqC;AAC1C,UAAM,EAAE,OAAO,GAAG,OAAO,IAAI;AAC7B,WAAO,KAAK,UAAwB,UAAU,QAAiB,EAAE,MAAM,CAAC;AAAA,EAC1E;AAAA;AAAA,EAGA,MAAM,UAAiD;AACrD,WAAO,KAAK,IAAiB,UAAU,mBAAmB,OAAO,QAAQ,CAAC,CAAC,EAAE;AAAA,EAC/E;AAAA;AAAA,EAGA,aAAa,UAAmD;AAC9D,WAAO,KAAK,IAAmB,UAAU,mBAAmB,OAAO,QAAQ,CAAC,CAAC,UAAU;AAAA,EACzF;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,UAAsD;AAC9D,WAAO,KAAK,IAAsB,UAAU,mBAAmB,OAAO,QAAQ,CAAC,CAAC,OAAO;AAAA,EACzF;AAAA;AAAA,EAIA,MAAM,SAA0C;AAC9C,WAAO,KAAK,IAAW,UAAU,mBAAmB,OAAO,OAAO,CAAC,CAAC,EAAE;AAAA,EACxE;AAAA;AAAA,EAGA,WAAW,SAA8D;AACvE,WAAO,KAAK,IAA+B,UAAU,mBAAmB,OAAO,OAAO,CAAC,CAAC,QAAQ;AAAA,EAClG;AAAA;AAAA,EAGA,YAAY,SAA8D;AACxE,WAAO,KAAK,IAA+B,UAAU,mBAAmB,OAAO,OAAO,CAAC,CAAC,SAAS;AAAA,EACnG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,gBAAgB,SAA+C;AAC7D,WAAO,KAAK,IAAgB,UAAU,mBAAmB,OAAO,OAAO,CAAC,CAAC,aAAa;AAAA,EACxF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OACE,OAAyE,CAAC,GACvC;AACnC,UAAM,EAAE,OAAO,WAAW,GAAG,KAAK,IAAI;AACtC,WAAO,KAAK,UAAiB,UAAU,EAAE,GAAG,MAAM,YAAY,UAAU,GAAY,EAAE,MAAM,CAAC;AAAA,EAC/F;AAAA,EAEA,MAAM,SAA0C;AAC9C,WAAO,KAAK,IAAW,UAAU,mBAAmB,OAAO,OAAO,CAAC,CAAC,EAAE;AAAA,EACxE;AAAA;AAAA,EAGA,gBACE,SACA,OAA2B,CAAC,GACyB;AACrD,WAAO,KAAK;AAAA,MACV,UAAU,mBAAmB,OAAO,OAAO,CAAC,CAAC;AAAA,MAC7C,CAAC;AAAA,MACD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAIA,SACE,OAAuE,CAAC,GAC5B;AAC5C,UAAM,EAAE,OAAO,GAAG,OAAO,IAAI;AAC7B,WAAO,KAAK,UAA0B,YAAY,QAAiB,EAAE,MAAM,CAAC;AAAA,EAC9E;AAAA;AAAA,EAGA,QAAQ,UAA6C;AACnD,WAAO,KAAK,IAAa,YAAY,mBAAmB,OAAO,QAAQ,CAAC,CAAC,EAAE;AAAA,EAC7E;AAAA;AAAA,EAGA,eAAe,UAAkD;AAC/D,WAAO,KAAK,IAAkB,YAAY,mBAAmB,OAAO,QAAQ,CAAC,CAAC,UAAU;AAAA,EAC1F;AAAA;AAAA,EAGA,aAAa,UAAmD;AAC9D,WAAO,KAAK,IAAmB,YAAY,mBAAmB,OAAO,QAAQ,CAAC,CAAC,QAAQ;AAAA,EACzF;AAAA;AAAA,EAGA,gBAAgB,UAA+D;AAC7E,WAAO,KAAK,IAA+B,YAAY,mBAAmB,OAAO,QAAQ,CAAC,CAAC,WAAW;AAAA,EACxG;AAAA,EAEA,kBAAkB,UAA0E;AAC1F,WAAO,KAAK;AAAA,MACV,YAAY,mBAAmB,OAAO,QAAQ,CAAC,CAAC;AAAA,IAClD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,SAAS,MAAM,OAAO,OAA0C,CAAC,GAA2B;AAC1F,WAAO,KAAK,IAAmB,YAAY,mBAAmB,GAAG,CAAC,IAAI,IAAa;AAAA,EACrF;AAAA,EAEA,iBAAiB,KAAa,UAAkB,OAA0B,CAAC,GAA2B;AACpG,WAAO,KAAK;AAAA,MACV,YAAY,mBAAmB,GAAG,CAAC,IAAI,mBAAmB,QAAQ,CAAC;AAAA,MACnE;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,YAAgD;AAC9C,WAAO,KAAK,IAA+B,WAAW;AAAA,EACxD;AAAA;AAAA,EAGA,WAAW,MAAM,OAAyC;AACxD,WAAO,KAAK,IAA6B,eAAe,mBAAmB,GAAG,CAAC,EAAE;AAAA,EACnF;AAAA;AAAA;AAAA,EAKA,sBAA0D;AACxD,WAAO,KAAK,IAA+B,sBAAsB;AAAA,EACnE;AAAA;AAAA,EAGA,gBAAgB,MAAM,OAAO,OAA6B,CAAC,GAA8B;AACvF,WAAO,KAAK,IAAsB,oBAAoB,mBAAmB,GAAG,CAAC,IAAI,IAAa;AAAA,EAChG;AAAA,EAEA,MAAM,SAA0C;AAC9C,WAAO,KAAK,IAAW,UAAU,mBAAmB,OAAO,OAAO,CAAC,CAAC,EAAE;AAAA,EACxE;AAAA;AAAA,EAGA,OAAO,GAA6C;AAClD,WAAO,KAAK,IAA6B,UAAU,EAAE,EAAE,CAAC;AAAA,EAC1D;AAAA;AAAA,EAGA,QAAwB;AACtB,WAAO,KAAK,IAAW,OAAO;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,eAAe,MAAM,OAAe;AAClC,QAAI,CAAC,KAAK,QAAQ;AAChB,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MAEF;AAAA,IACF;AACA,WAAO,GAAG,KAAK,OAAO,aAAa,mBAAmB,GAAG,CAAC,YAAY,mBAAmB,KAAK,MAAM,CAAC;AAAA,EACvG;AAAA;AAAA,EAIA,mBAA+C;AAC7C,WAAO,KAAK,IAAuB,mBAAmB;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,sBAAsB,KAAa,QAA6C;AACpF,UAAM,OAAgC,EAAE,IAAI;AAC5C,QAAI,QAAQ,OAAQ,MAAK,SAAS;AAClC,YAAQ,MAAM,KAAK,SAA0B,QAAQ,qBAAqB,EAAE,KAAK,CAAC,GAAG;AAAA,EACvF;AAAA,EAEA,MAAM,sBAAsB,YAA4C;AACtE,UAAM,KAAK,SAAkB,UAAU,qBAAqB,mBAAmB,OAAO,UAAU,CAAC,CAAC,EAAE;AAAA,EACtG;AAAA,EAEA,MAAM,oBAAoB,YAAuD;AAC/E,YACE,MAAM,KAAK;AAAA,MACT;AAAA,MACA,qBAAqB,mBAAmB,OAAO,UAAU,CAAC,CAAC;AAAA,IAC7D,GACA;AAAA,EACJ;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts","../src/client.ts"],"sourcesContent":["/** Kept in lockstep with package.json and the User-Agent by\n * `apps/api/src/surfaces.test.ts`. Bump all three together. */\nexport const VERSION = '0.3.0';\n","/**\n * Thin, dependency-free client for https://api.ufcalendar.com/v1.\n *\n * Every method returns the parsed `data` value of the JSON envelope\n * (`{\"data\": ..., \"meta\": ...}`); list endpoints are async generators that\n * follow cursor pagination for you. Errors throw `FightAPIError` carrying\n * the API's `code`, `message` and `requestId` — quote the request id when\n * you write to api@ufcalendar.com.\n *\n * Mirrors sdk/python/ufcalendar/client.py one method per endpoint, in\n * camelCase. The two clients are kept in lockstep by\n * `apps/api/src/surfaces.test.ts`.\n *\n * The API serves no betting odds, by design. Fighter `images` are Wikimedia\n * Commons / Creative Commons files — the `license` and `artist` fields you\n * receive must be displayed as a credit.\n */\nimport { VERSION } from './version';\nimport type {\n BroadcastRight,\n CareerBout,\n CareerStats,\n Envelope,\n EventChange,\n EventDetail,\n EventSummary,\n Fight,\n Fighter,\n FighterSummary,\n Judge,\n Meta,\n Org,\n Plans,\n PowerIndex,\n RankingsBoard,\n RateLimit,\n Scorecards,\n Usage,\n Venue,\n WebhookEndpoint,\n LiveState,\n LiveFrame,\n} from './types';\n\nexport const DEFAULT_BASE_URL = 'https://api.ufcalendar.com/v1';\n/** The live WebSocket (UFC fight nights, Pro plans and up) — the same document\n * `GET /v1/events/{id}/live` returns, pushed on every change. */\nexport const LIVE_WS_URL = 'wss://live.ufcalendar.com/v1';\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport interface FightAPIOptions {\n /** `ufcalendar_…` key from https://www.ufcalendar.com/account/api. Falls back\n * to `UFCALENDAR_API_KEY` / `UFCAL_API_KEY` in the environment. Only\n * `plans()` works without one. */\n apiKey?: string;\n /** Override for testing; defaults to the production `/v1`. */\n baseUrl?: string;\n /** Inject a fetch (a test double, an instrumented fetch, undici). */\n fetch?: typeof fetch;\n /** Per-request timeout. Default 30s. */\n timeoutMs?: number;\n /** Extra headers on every request. */\n headers?: Record<string, string>;\n}\n\n/** An error response from the Fight API. */\nexport class FightAPIError extends Error {\n readonly status: number;\n readonly code: string;\n readonly requestId: string | null;\n\n constructor(status: number, code: string, message: string, requestId: string | null = null) {\n super(`${status} ${code}: ${message}${requestId ? ` (request_id=${requestId})` : ''}`);\n this.name = 'FightAPIError';\n this.status = status;\n this.code = code;\n this.requestId = requestId;\n }\n}\n\nexport type QueryValue = string | number | boolean | null | undefined;\nexport type Query = Record<string, QueryValue>;\n\ninterface PageOptions {\n /** Stop after this many rows (across pages). */\n limit?: number;\n}\n\n/**\n * A browser forbids setting User-Agent on fetch, so we only set ours\n * off-browser. The test is `document`, NOT `navigator`: Node 21+ ships a\n * global `navigator`, so a navigator check would silently drop the header\n * on every modern Node runtime — which is the one place it works.\n */\nfunction canSetUserAgent(): boolean {\n return typeof document === 'undefined' && typeof window === 'undefined';\n}\n\nfunction envKey(): string | undefined {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;\n return env?.UFCALENDAR_API_KEY || env?.UFCAL_API_KEY;\n}\n\nexport class FightAPI {\n readonly baseUrl: string;\n readonly apiKey: string | undefined;\n\n /** `meta` of the most recent response (pagination cursor, generated_at …). */\n lastMeta: Meta | null = null;\n /** `X-RateLimit-*` of the most recent response. */\n lastRateLimit: RateLimit = { limit: null, remaining: null, reset: null };\n\n readonly #fetch: typeof fetch;\n readonly #timeoutMs: number;\n readonly #headers: Record<string, string>;\n\n constructor(apiKeyOrOptions?: string | FightAPIOptions, options: FightAPIOptions = {}) {\n const opts: FightAPIOptions =\n typeof apiKeyOrOptions === 'string'\n ? { ...options, apiKey: apiKeyOrOptions }\n : { ...options, ...(apiKeyOrOptions ?? {}) };\n this.apiKey = opts.apiKey ?? envKey();\n this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n this.#fetch = opts.fetch ?? globalThis.fetch;\n this.#timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.#headers = opts.headers ?? {};\n if (!this.#fetch) {\n throw new Error('No global fetch available. Use Node 18+ or pass { fetch }.');\n }\n }\n\n /* ---------------------------------------------------------------- core */\n\n #url(path: string, params?: Query): string {\n const url = new URL(`${this.baseUrl}/${path.replace(/^\\/+/, '')}`);\n for (const [k, v] of Object.entries(params ?? {})) {\n if (v === undefined || v === null) continue;\n url.searchParams.set(k, String(v));\n }\n return url.toString();\n }\n\n async #request<T>(\n method: string,\n path: string,\n { params, body, open = false }: { params?: Query; body?: unknown; open?: boolean } = {},\n ): Promise<Envelope<T>> {\n if (!this.apiKey && !open) {\n throw new FightAPIError(\n 401,\n 'no_api_key',\n 'No API key. Pass new FightAPI(\"ufcalendar_...\") or set UFCALENDAR_API_KEY. ' +\n 'Keys (and the free 1-day trial) live at https://www.ufcalendar.com/account/api',\n );\n }\n const headers: Record<string, string> = { Accept: 'application/json', ...this.#headers };\n if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;\n if (canSetUserAgent()) headers['User-Agent'] = `ufcalendar-typescript/${VERSION}`;\n if (body !== undefined) headers['Content-Type'] = 'application/json';\n\n let res: Response;\n try {\n res = await this.#fetch(this.#url(path, params), {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: AbortSignal.timeout(this.#timeoutMs),\n });\n } catch (e) {\n const err = e as { name?: string; message?: string };\n if (err?.name === 'TimeoutError') {\n throw new FightAPIError(0, 'timeout', `Request to ${path} timed out after ${this.#timeoutMs}ms`);\n }\n throw new FightAPIError(0, 'network_error', err?.message ?? String(e));\n }\n\n this.lastRateLimit = {\n limit: res.headers.get('X-RateLimit-Limit'),\n remaining: res.headers.get('X-RateLimit-Remaining'),\n reset: res.headers.get('X-RateLimit-Reset'),\n };\n\n if (res.status === 204) {\n this.lastMeta = null;\n return { data: undefined as T };\n }\n\n const text = await res.text();\n let parsed: unknown = undefined;\n try {\n parsed = text ? JSON.parse(text) : undefined;\n } catch {\n parsed = undefined;\n }\n\n if (!res.ok) {\n const err = (parsed as { error?: { code?: string; message?: string; request_id?: string } } | undefined)?.error;\n if (err) {\n throw new FightAPIError(\n res.status,\n String(err.code ?? 'error'),\n String(err.message ?? text.slice(0, 200)),\n err.request_id ?? res.headers.get('x-request-id'),\n );\n }\n throw new FightAPIError(res.status, 'http_error', text.slice(0, 200), res.headers.get('x-request-id'));\n }\n\n const env = (parsed ?? {}) as Envelope<T>;\n this.lastMeta = env.meta ?? null;\n return env;\n }\n\n /** Raw GET returning the `data` value. Escape hatch for new endpoints. */\n async get<T = unknown>(path: string, params?: Query): Promise<T> {\n return (await this.#request<T>('GET', path, { params })).data;\n }\n\n /** Raw GET returning the whole `{data, meta}` envelope. */\n async getWithMeta<T = unknown>(path: string, params?: Query): Promise<Envelope<T>> {\n return this.#request<T>('GET', path, { params });\n }\n\n async *#paginate<T>(path: string, params: Query, opts: PageOptions = {}): AsyncGenerator<T, void, void> {\n // Never fetch a bigger page than the caller wants rows: `limit: 3` used\n // to pull 100 rows and throw 97 away, burning the caller's quota on a\n // request they did not ask for (and, on a trial key, a noticeable slice\n // of the 100 they get).\n const pageSize = Math.min(100, opts.limit ?? 100);\n const query: Query = { limit: pageSize, ...params };\n let seen = 0;\n for (;;) {\n const env = await this.#request<T[]>('GET', path, { params: query });\n for (const row of env.data ?? []) {\n yield row;\n seen += 1;\n if (opts.limit !== undefined && seen >= opts.limit) return;\n }\n const cursor = env.meta?.pagination?.next_cursor;\n if (!cursor) return;\n query.cursor = cursor;\n }\n }\n\n /* --------------------------------------------------------------- plans */\n\n /**\n * Plans, quotas, the free 1-day trial rule, the MCP endpoint and the doc\n * links. The only endpoint that answers without a credential.\n */\n async plans(): Promise<Plans> {\n return (await this.#request<Plans>('GET', 'plans', { open: true })).data;\n }\n\n /* ---------------------------------------------------------------- orgs */\n\n /** Launch orgs with capability flags (stats / rounds / rankings / broadcasts / predictions). */\n orgs(): Promise<Org[]> {\n return this.get<Org[]>('orgs');\n }\n\n org(slug: string): Promise<Org> {\n return this.get<Org>(`orgs/${encodeURIComponent(slug)}`);\n }\n\n /* -------------------------------------------------------------- events */\n\n /**\n * Schedule + results. Bare call = the upcoming calendar, soonest first.\n * `status: 'completed'` (or `order: 'desc'`) browses the archive\n * newest-first. `from` / `to` are `YYYY-MM-DD`.\n */\n events(\n opts: {\n org?: string;\n status?: string;\n from?: string;\n to?: string;\n order?: 'asc' | 'desc';\n limit?: number;\n } = {},\n ): AsyncGenerator<EventSummary, void, void> {\n const { limit, ...params } = opts;\n return this.#paginate<EventSummary>('events', params as Query, { limit });\n }\n\n /** One event with its full fight card, venue and broadcasts. */\n event(idOrSlug: string | number): Promise<EventDetail> {\n return this.get<EventDetail>(`events/${encodeURIComponent(String(idOrSlug))}`);\n }\n\n /** Card-change diff log (fight added/removed, opponent swapped, date moved, profile merged). */\n eventChanges(idOrSlug: string | number): Promise<EventChange[]> {\n return this.get<EventChange[]>(`events/${encodeURIComponent(String(idOrSlug))}/changes`);\n }\n\n /** Latest real-time LiveState snapshot on fight night (Pro plans and up), or\n * null when nothing is being streamed. The WebSocket at\n * wss://live.ufcalendar.com/v1 pushes the same document as it changes — see\n * `subscribeLive()`. */\n eventLive(idOrSlug: string | number): Promise<LiveState | null> {\n return this.get<LiveState | null>(`events/${encodeURIComponent(String(idOrSlug))}/live`);\n }\n\n /**\n * Subscribe to the live WebSocket and receive every frame (Pro plans and up).\n *\n * The UFC live API: opens `wss://live.ufcalendar.com/v1?key=…`, sends\n * `{\"action\":\"subscribe\",\"event\":<slug>}` and calls `onFrame` with each\n * `LiveFrame` — round, running clock, unofficial in-fight statistics and the\n * action timeline, the same `LiveState` `eventLive()` returns.\n *\n * ```ts\n * const stop = api.subscribeLive('ufc-331', (f) => console.log(f.type), { until: 'final' });\n * ```\n *\n * Uses the global `WebSocket` (browser, Node >= 22) unless you inject one\n * (`ws` on older Node). A dropped socket reconnects and re-subscribes ONCE.\n *\n * @returns an unsubscribe function — call it to close the socket for good.\n */\n subscribeLive(\n event: string,\n onFrame: (frame: LiveFrame) => void,\n opts: { until?: 'final'; WebSocketImpl?: typeof WebSocket } = {},\n ): () => void {\n const Impl = opts.WebSocketImpl ?? (globalThis as { WebSocket?: typeof WebSocket }).WebSocket;\n if (!Impl) {\n throw new Error(\n 'subscribeLive() needs a WebSocket: use Node >= 22 (or a browser), or pass ' +\n \"{ WebSocketImpl } — e.g. `import WebSocket from 'ws'`.\",\n );\n }\n if (!this.apiKey) {\n throw new Error(\n 'No API key. The live stream authenticates with ?key=. Keys (and the free ' +\n '1-day trial) live at https://www.ufcalendar.com/account/api',\n );\n }\n const url = `${LIVE_WS_URL}?key=${encodeURIComponent(this.apiKey)}`;\n let done = false;\n let reconnects = 0;\n let sock: WebSocket | null = null;\n\n const open = () => {\n const ws = new Impl(url);\n sock = ws;\n ws.onopen = () => {\n try {\n ws.send(JSON.stringify({ action: 'subscribe', event }));\n } catch {\n /* the close handler reconnects */\n }\n };\n ws.onmessage = (ev: MessageEvent) => {\n let frame: LiveFrame;\n try {\n frame = JSON.parse(typeof ev.data === 'string' ? ev.data : String(ev.data)) as LiveFrame;\n } catch {\n return; // a half-frame is not worth killing the night over\n }\n if (!frame || typeof frame !== 'object') return;\n onFrame(frame);\n if (opts.until === 'final' && frame.type === 'fight.final') {\n done = true;\n try {\n ws.close();\n } catch {\n /* already gone */\n }\n }\n };\n ws.onclose = () => {\n if (done || reconnects >= 1) return;\n reconnects += 1;\n open();\n };\n };\n open();\n\n return () => {\n done = true;\n try {\n sock?.close();\n } catch {\n /* already gone */\n }\n };\n }\n\n /* -------------------------------------------------------------- fights */\n\n fight(fightId: number | string): Promise<Fight> {\n return this.get<Fight>(`fights/${encodeURIComponent(String(fightId))}`);\n }\n\n /** Per-fight totals for both corners. */\n fightStats(fightId: number | string): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>(`fights/${encodeURIComponent(String(fightId))}/stats`);\n }\n\n /** Round-by-round stat lines for both corners. */\n fightRounds(fightId: number | string): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>(`fights/${encodeURIComponent(String(fightId))}/rounds`);\n }\n\n /**\n * The judges' scorecards for a bout — the official commission record:\n * decision type, point deductions, and one card per judge with every\n * round, the totals and `winner_fighter_id`. Scores are oriented to\n * `fighter_a_id` / `fighter_b_id`, both repeated on the payload.\n *\n * Check `scores_known` on a card before charting totals: when it is false\n * the commission published only the outcome. Throws 404 for a bout that\n * did not go to the judges.\n */\n fightScorecards(fightId: number | string): Promise<Scorecards> {\n return this.get<Scorecards>(`fights/${encodeURIComponent(String(fightId))}/scorecards`);\n }\n\n /* -------------------------------------------------------------- judges */\n\n /**\n * Every official who has scored a launch-org bout, busiest first. Rates\n * mean little below ~10 fights; pass `minFights: 10`.\n */\n judges(\n opts: { q?: string; org?: string; minFights?: number; limit?: number } = {},\n ): AsyncGenerator<Judge, void, void> {\n const { limit, minFights, ...rest } = opts;\n return this.#paginate<Judge>('judges', { ...rest, min_fights: minFights } as Query, { limit });\n }\n\n judge(judgeId: number | string): Promise<Judge> {\n return this.get<Judge>(`judges/${encodeURIComponent(String(judgeId))}`);\n }\n\n /** Every card this judge has turned in, newest first. */\n judgeScorecards(\n judgeId: number | string,\n opts: { limit?: number } = {},\n ): AsyncGenerator<Record<string, unknown>, void, void> {\n return this.#paginate<Record<string, unknown>>(\n `judges/${encodeURIComponent(String(judgeId))}/scorecards`,\n {},\n opts,\n );\n }\n\n /* ------------------------------------------------------------ fighters */\n\n fighters(\n opts: { q?: string; org?: string; country?: string; limit?: number } = {},\n ): AsyncGenerator<FighterSummary, void, void> {\n const { limit, ...params } = opts;\n return this.#paginate<FighterSummary>('fighters', params as Query, { limit });\n }\n\n /** Bio, records, career stats, Power Index and CC-licensed images. */\n fighter(idOrSlug: string | number): Promise<Fighter> {\n return this.get<Fighter>(`fighters/${encodeURIComponent(String(idOrSlug))}`);\n }\n\n /** Complete multi-promotion career timeline. */\n fighterHistory(idOrSlug: string | number): Promise<CareerBout[]> {\n return this.get<CareerBout[]>(`fighters/${encodeURIComponent(String(idOrSlug))}/history`);\n }\n\n /** Career statistics, one source-stamped row per scope (`pro-mma`, `ufc-only` …). */\n fighterStats(idOrSlug: string | number): Promise<CareerStats[]> {\n return this.get<CareerStats[]>(`fighters/${encodeURIComponent(String(idOrSlug))}/stats`);\n }\n\n /** Every official ranking row the fighter ever held, newest first. */\n fighterRankings(idOrSlug: string | number): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>(`fighters/${encodeURIComponent(String(idOrSlug))}/rankings`);\n }\n\n fighterPowerIndex(idOrSlug: string | number): Promise<PowerIndex & Record<string, unknown>> {\n return this.get<PowerIndex & Record<string, unknown>>(\n `fighters/${encodeURIComponent(String(idOrSlug))}/power-index`,\n );\n }\n\n /* ------------------------------------------------------------ rankings */\n\n /**\n * Official board, point-in-time. `date: 'YYYY-MM-DD'` returns the board\n * that was valid on that day (UFC history back to 2013; rank 0 = champion).\n */\n rankings(org = 'ufc', opts: { date?: string; board?: string } = {}): Promise<RankingsBoard> {\n return this.get<RankingsBoard>(`rankings/${encodeURIComponent(org)}`, opts as Query);\n }\n\n divisionRankings(org: string, division: string, opts: { date?: string } = {}): Promise<RankingsBoard> {\n return this.get<RankingsBoard>(\n `rankings/${encodeURIComponent(org)}/${encodeURIComponent(division)}`,\n opts as Query,\n );\n }\n\n /** Current champions across every launch org. */\n champions(): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>('champions');\n }\n\n /** UFCalendar Power Index board. */\n powerIndex(org = 'ufc'): Promise<Record<string, unknown>> {\n return this.get<Record<string, unknown>>(`power-index/${encodeURIComponent(org)}`);\n }\n\n /* ---------------------------------------------------------------- misc */\n\n /** Model win probabilities for upcoming UFC bouts. */\n predictionsUpcoming(): Promise<Record<string, unknown>[]> {\n return this.get<Record<string, unknown>[]>('predictions/upcoming');\n }\n\n /** Who airs the promotion, per ISO-2 country. */\n broadcastRights(org = 'ufc', opts: { country?: string } = {}): Promise<BroadcastRight[]> {\n return this.get<BroadcastRight[]>(`broadcast-rights/${encodeURIComponent(org)}`, opts as Query);\n }\n\n venue(venueId: number | string): Promise<Venue> {\n return this.get<Venue>(`venues/${encodeURIComponent(String(venueId))}`);\n }\n\n /** Typeahead across fighters and events. */\n search(q: string): Promise<Record<string, unknown>> {\n return this.get<Record<string, unknown>>('search', { q });\n }\n\n /** Your key's month-to-date quota usage. */\n usage(): Promise<Usage> {\n return this.get<Usage>('usage');\n }\n\n /**\n * Subscribable ICS feed URL for calendar apps (authenticates via `?key=`).\n *\n * Throws without a key rather than returning `?key=`: that empty URL is\n * pasted into a calendar app, fails there hours later, and the failure\n * surfaces nowhere near this call.\n */\n calendarIcsUrl(org = 'ufc'): string {\n if (!this.apiKey) {\n throw new FightAPIError(\n 401,\n 'no_api_key',\n 'No API key. Pass new FightAPI(\"ufcalendar_...\") or set UFCALENDAR_API_KEY. ' +\n 'Keys (and the free 1-day trial) live at https://www.ufcalendar.com/account/api',\n );\n }\n return `${this.baseUrl}/calendar/${encodeURIComponent(org)}.ics?key=${encodeURIComponent(this.apiKey)}`;\n }\n\n /* ------------------------------------------------------------ webhooks */\n\n webhookEndpoints(): Promise<WebhookEndpoint[]> {\n return this.get<WebhookEndpoint[]>('webhook-endpoints');\n }\n\n /**\n * Register a signed webhook (Pro and up). `events` ⊆ `event.announced`,\n * `fight.result`, `card.changed`, `event.completed`. The signing secret is\n * returned ONCE, in this response.\n */\n async createWebhookEndpoint(url: string, events?: string[]): Promise<WebhookEndpoint> {\n const body: Record<string, unknown> = { url };\n if (events?.length) body.events = events;\n return (await this.#request<WebhookEndpoint>('POST', 'webhook-endpoints', { body })).data;\n }\n\n async deleteWebhookEndpoint(endpointId: number | string): Promise<void> {\n await this.#request<unknown>('DELETE', `webhook-endpoints/${encodeURIComponent(String(endpointId))}`);\n }\n\n async rotateWebhookSecret(endpointId: number | string): Promise<WebhookEndpoint> {\n return (\n await this.#request<WebhookEndpoint>(\n 'POST',\n `webhook-endpoints/${encodeURIComponent(String(endpointId))}/rotate-secret`,\n )\n ).data;\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ufcalendar/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript client for the UFCalendar Fight API: UFC, PFL, OKTAGON, BKFC and RIZIN events, fight cards, results, per-round stats, fighter careers, judges' scorecards and UFC rankings history since 2013.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,7 +26,10 @@
26
26
  "mcp-server",
27
27
  "ufc-mcp",
28
28
  "mma-mcp",
29
- "fight-api"
29
+ "fight-api",
30
+ "websocket",
31
+ "live-stream",
32
+ "ufc-live"
30
33
  ],
31
34
  "sideEffects": false,
32
35
  "main": "./dist/index.cjs",