@ufcalendar/sdk 0.1.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/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/index.cjs +363 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3112 -0
- package/dist/index.d.ts +3112 -0
- package/dist/index.js +333 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 UFCalendar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @ufcalendar/sdk — TypeScript client for the UFCalendar Fight API
|
|
2
|
+
|
|
3
|
+
The [UFCalendar Fight API](https://www.ufcalendar.com/developers) is a REST API for MMA data: **UFC, PFL, OKTAGON, BKFC and RIZIN** events, full fight cards, results within minutes, per-fight and round-by-round statistics, complete fighter careers, the only **UFC rankings API with point-in-time history back to 2013**, and the only one serving **judges' scorecards** — every official, every round. This package is a dependency-free `fetch` wrapper over it: one method per endpoint, cursor pagination handled for you, ESM + CJS, Node 18+, browsers and edge runtimes.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @ufcalendar/sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Get a key (free 1-day trial, 100 requests, no card) at <https://www.ufcalendar.com/account/api?trial=1>. Paid plans from $19/month for 30,000 requests (Pro $49 for 200,000, Business $149 for 1,000,000) — hard caps, no overage.
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { FightAPI } from '@ufcalendar/sdk';
|
|
15
|
+
|
|
16
|
+
const api = new FightAPI(process.env.UFCALENDAR_API_KEY); // or new FightAPI({ apiKey })
|
|
17
|
+
|
|
18
|
+
// Upcoming UFC schedule, soonest first
|
|
19
|
+
for await (const ev of api.events({ org: 'ufc', limit: 5 })) {
|
|
20
|
+
console.log(ev.starts_at, ev.title, ev.is_ppv ? 'PPV' : '');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Full card + results of the newest completed UFC event
|
|
24
|
+
const [latest] = await Array.fromAsync(api.events({ org: 'ufc', status: 'completed', limit: 1 }));
|
|
25
|
+
const card = await api.event(latest!.slug);
|
|
26
|
+
for (const f of card.card) {
|
|
27
|
+
if (f.result) {
|
|
28
|
+
console.log(f.fighter_a?.name, 'vs', f.fighter_b?.name, '->', f.result.method, `R${f.result.round}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The judges' scorecards for one bout — the official commission record
|
|
33
|
+
const cards = await api.fightScorecards(card.card[0]!.id);
|
|
34
|
+
|
|
35
|
+
// UFC rankings on any date since February 2013 (rank 0 = champion)
|
|
36
|
+
const board = await api.rankings('ufc', { date: '2016-11-14' });
|
|
37
|
+
|
|
38
|
+
// A fighter's complete multi-promotion career
|
|
39
|
+
const history = await api.fighterHistory('islam-makhachev');
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
List endpoints are **async generators** that follow `meta.pagination.next_cursor` for you; pass `limit` to stop after N rows. Everything else returns the envelope's `data`. The last response's `meta` and rate-limit headers stay on the client (`api.lastMeta`, `api.lastRateLimit`).
|
|
43
|
+
|
|
44
|
+
## What's covered
|
|
45
|
+
|
|
46
|
+
| Method | Endpoint |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `plans()` | `GET /v1/plans` — plans, quotas, trial terms, MCP endpoint (no key required) |
|
|
49
|
+
| `orgs()` / `org(slug)` | `GET /v1/orgs` / `…/{slug}` |
|
|
50
|
+
| `events({ org, status, from, to, order, limit })` | `GET /v1/events` (paginated) |
|
|
51
|
+
| `event(slug)` / `eventChanges(slug)` | `GET /v1/events/{slug}` / `…/changes` |
|
|
52
|
+
| `fight(id)` / `fightStats(id)` / `fightRounds(id)` | `GET /v1/fights/{id}` / `…/stats` / `…/rounds` |
|
|
53
|
+
| `fightScorecards(id)` | `GET /v1/fights/{id}/scorecards` — judges, rounds, totals, deductions |
|
|
54
|
+
| `judges({ q, org, minFights })` / `judge(id)` / `judgeScorecards(id)` | `GET /v1/judges` / `…/{id}` / `…/{id}/scorecards` |
|
|
55
|
+
| `fighters({ q, org, country })` / `fighter(slug)` | `GET /v1/fighters` / `…/{slug}` |
|
|
56
|
+
| `fighterHistory` / `fighterStats` / `fighterRankings` / `fighterPowerIndex` | `GET /v1/fighters/{slug}/…` |
|
|
57
|
+
| `rankings(org, { date })` / `divisionRankings(org, division)` / `champions()` | `GET /v1/rankings/…` / `/v1/champions` |
|
|
58
|
+
| `powerIndex(org)` / `predictionsUpcoming()` | `GET /v1/power-index/{org}` / `/v1/predictions/upcoming` |
|
|
59
|
+
| `broadcastRights(org, { country })` / `venue(id)` / `search(q)` / `usage()` | misc |
|
|
60
|
+
| `webhookEndpoints()` / `createWebhookEndpoint(url, events)` / `rotateWebhookSecret(id)` / `deleteWebhookEndpoint(id)` | `/v1/webhook-endpoints` (Pro+) |
|
|
61
|
+
| `calendarIcsUrl(org)` | `GET /v1/calendar/{org}.ics` (URL builder — no request) |
|
|
62
|
+
| `get(path, params)` / `getWithMeta(path, params)` | escape hatch for anything new |
|
|
63
|
+
|
|
64
|
+
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
|
+
|
|
66
|
+
## Agents and MCP
|
|
67
|
+
|
|
68
|
+
The same data is a Model Context Protocol server, so an agent can call it without an HTTP client:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
claude mcp add --transport http ufcalendar https://api.ufcalendar.com/mcp \
|
|
72
|
+
--header "Authorization: Bearer $UFCALENDAR_API_KEY"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
There is an installable agent skill too: `npx skills add UFCalendar/fight-api-skill`. Agent quickstart: <https://www.ufcalendar.com/developers/agents>.
|
|
76
|
+
|
|
77
|
+
## Options
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
new FightAPI({
|
|
81
|
+
apiKey, // or UFCALENDAR_API_KEY / UFCAL_API_KEY in the environment
|
|
82
|
+
baseUrl, // default https://api.ufcalendar.com/v1
|
|
83
|
+
fetch, // inject your own fetch
|
|
84
|
+
timeoutMs, // default 30_000
|
|
85
|
+
headers, // extra headers on every request
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Errors and rate limits
|
|
90
|
+
|
|
91
|
+
Every error throws `FightAPIError` with `.status`, `.code`, `.message` and `.requestId` — quote the request id when you write to api@ufcalendar.com. After each call `api.lastRateLimit` holds the `X-RateLimit-*` headers (monthly quota).
|
|
92
|
+
|
|
93
|
+
## Notes
|
|
94
|
+
|
|
95
|
+
- No betting odds are served, by design.
|
|
96
|
+
- Fighter `images` are Wikimedia Commons / Creative Commons files: displaying the `license` and `artist` fields as a credit is a licence requirement, not a style preference.
|
|
97
|
+
- Scorecards are the official commission record. Media-member and fan scorecards are not part of the API.
|
|
98
|
+
- Not affiliated with UFC, Zuffa, TKO or any promotion. Terms: <https://www.ufcalendar.com/developers/terms>
|
|
99
|
+
|
|
100
|
+
Full reference: <https://api.ufcalendar.com/docs> · OpenAPI 3.1: <https://api.ufcalendar.com/openapi.json> · Also on [PyPI (Python client)](https://pypi.org/project/ufcalendar/), [RapidAPI Hub](https://rapidapi.com/ceo-SP8r6F1JT/api/ufc-and-mma-fight-data-by-ufcalendar) and [Postman](https://www.postman.com/ceo-5d84eedc/workspace/ufcalendar-fight-api)
|
|
101
|
+
|
|
102
|
+
MIT licensed.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
24
|
+
FightAPI: () => FightAPI,
|
|
25
|
+
FightAPIError: () => FightAPIError,
|
|
26
|
+
VERSION: () => VERSION
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/version.ts
|
|
31
|
+
var VERSION = "0.1.0";
|
|
32
|
+
|
|
33
|
+
// src/client.ts
|
|
34
|
+
var DEFAULT_BASE_URL = "https://api.ufcalendar.com/v1";
|
|
35
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
36
|
+
var FightAPIError = class extends Error {
|
|
37
|
+
status;
|
|
38
|
+
code;
|
|
39
|
+
requestId;
|
|
40
|
+
constructor(status, code, message, requestId = null) {
|
|
41
|
+
super(`${status} ${code}: ${message}${requestId ? ` (request_id=${requestId})` : ""}`);
|
|
42
|
+
this.name = "FightAPIError";
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.requestId = requestId;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function canSetUserAgent() {
|
|
49
|
+
return typeof document === "undefined" && typeof window === "undefined";
|
|
50
|
+
}
|
|
51
|
+
function envKey() {
|
|
52
|
+
const env = globalThis.process?.env;
|
|
53
|
+
return env?.UFCALENDAR_API_KEY || env?.UFCAL_API_KEY;
|
|
54
|
+
}
|
|
55
|
+
var FightAPI = class {
|
|
56
|
+
baseUrl;
|
|
57
|
+
apiKey;
|
|
58
|
+
/** `meta` of the most recent response (pagination cursor, generated_at …). */
|
|
59
|
+
lastMeta = null;
|
|
60
|
+
/** `X-RateLimit-*` of the most recent response. */
|
|
61
|
+
lastRateLimit = { limit: null, remaining: null, reset: null };
|
|
62
|
+
#fetch;
|
|
63
|
+
#timeoutMs;
|
|
64
|
+
#headers;
|
|
65
|
+
constructor(apiKeyOrOptions, options = {}) {
|
|
66
|
+
const opts = typeof apiKeyOrOptions === "string" ? { ...options, apiKey: apiKeyOrOptions } : { ...options, ...apiKeyOrOptions ?? {} };
|
|
67
|
+
this.apiKey = opts.apiKey ?? envKey();
|
|
68
|
+
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
69
|
+
this.#fetch = opts.fetch ?? globalThis.fetch;
|
|
70
|
+
this.#timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
71
|
+
this.#headers = opts.headers ?? {};
|
|
72
|
+
if (!this.#fetch) {
|
|
73
|
+
throw new Error("No global fetch available. Use Node 18+ or pass { fetch }.");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/* ---------------------------------------------------------------- core */
|
|
77
|
+
#url(path, params) {
|
|
78
|
+
const url = new URL(`${this.baseUrl}/${path.replace(/^\/+/, "")}`);
|
|
79
|
+
for (const [k, v] of Object.entries(params ?? {})) {
|
|
80
|
+
if (v === void 0 || v === null) continue;
|
|
81
|
+
url.searchParams.set(k, String(v));
|
|
82
|
+
}
|
|
83
|
+
return url.toString();
|
|
84
|
+
}
|
|
85
|
+
async #request(method, path, { params, body, open = false } = {}) {
|
|
86
|
+
if (!this.apiKey && !open) {
|
|
87
|
+
throw new FightAPIError(
|
|
88
|
+
401,
|
|
89
|
+
"no_api_key",
|
|
90
|
+
'No API key. Pass new FightAPI("ufcalendar_...") or set UFCALENDAR_API_KEY. Keys (and the free 1-day trial) live at https://www.ufcalendar.com/account/api'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const headers = { Accept: "application/json", ...this.#headers };
|
|
94
|
+
if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
|
|
95
|
+
if (canSetUserAgent()) headers["User-Agent"] = `ufcalendar-typescript/${VERSION}`;
|
|
96
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
97
|
+
let res;
|
|
98
|
+
try {
|
|
99
|
+
res = await this.#fetch(this.#url(path, params), {
|
|
100
|
+
method,
|
|
101
|
+
headers,
|
|
102
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
103
|
+
signal: AbortSignal.timeout(this.#timeoutMs)
|
|
104
|
+
});
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const err = e;
|
|
107
|
+
if (err?.name === "TimeoutError") {
|
|
108
|
+
throw new FightAPIError(0, "timeout", `Request to ${path} timed out after ${this.#timeoutMs}ms`);
|
|
109
|
+
}
|
|
110
|
+
throw new FightAPIError(0, "network_error", err?.message ?? String(e));
|
|
111
|
+
}
|
|
112
|
+
this.lastRateLimit = {
|
|
113
|
+
limit: res.headers.get("X-RateLimit-Limit"),
|
|
114
|
+
remaining: res.headers.get("X-RateLimit-Remaining"),
|
|
115
|
+
reset: res.headers.get("X-RateLimit-Reset")
|
|
116
|
+
};
|
|
117
|
+
if (res.status === 204) {
|
|
118
|
+
this.lastMeta = null;
|
|
119
|
+
return { data: void 0 };
|
|
120
|
+
}
|
|
121
|
+
const text = await res.text();
|
|
122
|
+
let parsed = void 0;
|
|
123
|
+
try {
|
|
124
|
+
parsed = text ? JSON.parse(text) : void 0;
|
|
125
|
+
} catch {
|
|
126
|
+
parsed = void 0;
|
|
127
|
+
}
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
const err = parsed?.error;
|
|
130
|
+
if (err) {
|
|
131
|
+
throw new FightAPIError(
|
|
132
|
+
res.status,
|
|
133
|
+
String(err.code ?? "error"),
|
|
134
|
+
String(err.message ?? text.slice(0, 200)),
|
|
135
|
+
err.request_id ?? res.headers.get("x-request-id")
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
throw new FightAPIError(res.status, "http_error", text.slice(0, 200), res.headers.get("x-request-id"));
|
|
139
|
+
}
|
|
140
|
+
const env = parsed ?? {};
|
|
141
|
+
this.lastMeta = env.meta ?? null;
|
|
142
|
+
return env;
|
|
143
|
+
}
|
|
144
|
+
/** Raw GET returning the `data` value. Escape hatch for new endpoints. */
|
|
145
|
+
async get(path, params) {
|
|
146
|
+
return (await this.#request("GET", path, { params })).data;
|
|
147
|
+
}
|
|
148
|
+
/** Raw GET returning the whole `{data, meta}` envelope. */
|
|
149
|
+
async getWithMeta(path, params) {
|
|
150
|
+
return this.#request("GET", path, { params });
|
|
151
|
+
}
|
|
152
|
+
async *#paginate(path, params, opts = {}) {
|
|
153
|
+
const pageSize = Math.min(100, opts.limit ?? 100);
|
|
154
|
+
const query = { limit: pageSize, ...params };
|
|
155
|
+
let seen = 0;
|
|
156
|
+
for (; ; ) {
|
|
157
|
+
const env = await this.#request("GET", path, { params: query });
|
|
158
|
+
for (const row of env.data ?? []) {
|
|
159
|
+
yield row;
|
|
160
|
+
seen += 1;
|
|
161
|
+
if (opts.limit !== void 0 && seen >= opts.limit) return;
|
|
162
|
+
}
|
|
163
|
+
const cursor = env.meta?.pagination?.next_cursor;
|
|
164
|
+
if (!cursor) return;
|
|
165
|
+
query.cursor = cursor;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/* --------------------------------------------------------------- plans */
|
|
169
|
+
/**
|
|
170
|
+
* Plans, quotas, the free 1-day trial rule, the MCP endpoint and the doc
|
|
171
|
+
* links. The only endpoint that answers without a credential.
|
|
172
|
+
*/
|
|
173
|
+
async plans() {
|
|
174
|
+
return (await this.#request("GET", "plans", { open: true })).data;
|
|
175
|
+
}
|
|
176
|
+
/* ---------------------------------------------------------------- orgs */
|
|
177
|
+
/** Launch orgs with capability flags (stats / rounds / rankings / broadcasts / predictions). */
|
|
178
|
+
orgs() {
|
|
179
|
+
return this.get("orgs");
|
|
180
|
+
}
|
|
181
|
+
org(slug) {
|
|
182
|
+
return this.get(`orgs/${encodeURIComponent(slug)}`);
|
|
183
|
+
}
|
|
184
|
+
/* -------------------------------------------------------------- events */
|
|
185
|
+
/**
|
|
186
|
+
* Schedule + results. Bare call = the upcoming calendar, soonest first.
|
|
187
|
+
* `status: 'completed'` (or `order: 'desc'`) browses the archive
|
|
188
|
+
* newest-first. `from` / `to` are `YYYY-MM-DD`.
|
|
189
|
+
*/
|
|
190
|
+
events(opts = {}) {
|
|
191
|
+
const { limit, ...params } = opts;
|
|
192
|
+
return this.#paginate("events", params, { limit });
|
|
193
|
+
}
|
|
194
|
+
/** One event with its full fight card, venue and broadcasts. */
|
|
195
|
+
event(idOrSlug) {
|
|
196
|
+
return this.get(`events/${encodeURIComponent(String(idOrSlug))}`);
|
|
197
|
+
}
|
|
198
|
+
/** Card-change diff log (fight added/removed, opponent swapped, date moved, profile merged). */
|
|
199
|
+
eventChanges(idOrSlug) {
|
|
200
|
+
return this.get(`events/${encodeURIComponent(String(idOrSlug))}/changes`);
|
|
201
|
+
}
|
|
202
|
+
/* -------------------------------------------------------------- fights */
|
|
203
|
+
fight(fightId) {
|
|
204
|
+
return this.get(`fights/${encodeURIComponent(String(fightId))}`);
|
|
205
|
+
}
|
|
206
|
+
/** Per-fight totals for both corners. */
|
|
207
|
+
fightStats(fightId) {
|
|
208
|
+
return this.get(`fights/${encodeURIComponent(String(fightId))}/stats`);
|
|
209
|
+
}
|
|
210
|
+
/** Round-by-round stat lines for both corners. */
|
|
211
|
+
fightRounds(fightId) {
|
|
212
|
+
return this.get(`fights/${encodeURIComponent(String(fightId))}/rounds`);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The judges' scorecards for a bout — the official commission record:
|
|
216
|
+
* decision type, point deductions, and one card per judge with every
|
|
217
|
+
* round, the totals and `winner_fighter_id`. Scores are oriented to
|
|
218
|
+
* `fighter_a_id` / `fighter_b_id`, both repeated on the payload.
|
|
219
|
+
*
|
|
220
|
+
* Check `scores_known` on a card before charting totals: when it is false
|
|
221
|
+
* the commission published only the outcome. Throws 404 for a bout that
|
|
222
|
+
* did not go to the judges.
|
|
223
|
+
*/
|
|
224
|
+
fightScorecards(fightId) {
|
|
225
|
+
return this.get(`fights/${encodeURIComponent(String(fightId))}/scorecards`);
|
|
226
|
+
}
|
|
227
|
+
/* -------------------------------------------------------------- judges */
|
|
228
|
+
/**
|
|
229
|
+
* Every official who has scored a launch-org bout, busiest first. Rates
|
|
230
|
+
* mean little below ~10 fights; pass `minFights: 10`.
|
|
231
|
+
*/
|
|
232
|
+
judges(opts = {}) {
|
|
233
|
+
const { limit, minFights, ...rest } = opts;
|
|
234
|
+
return this.#paginate("judges", { ...rest, min_fights: minFights }, { limit });
|
|
235
|
+
}
|
|
236
|
+
judge(judgeId) {
|
|
237
|
+
return this.get(`judges/${encodeURIComponent(String(judgeId))}`);
|
|
238
|
+
}
|
|
239
|
+
/** Every card this judge has turned in, newest first. */
|
|
240
|
+
judgeScorecards(judgeId, opts = {}) {
|
|
241
|
+
return this.#paginate(
|
|
242
|
+
`judges/${encodeURIComponent(String(judgeId))}/scorecards`,
|
|
243
|
+
{},
|
|
244
|
+
opts
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
/* ------------------------------------------------------------ fighters */
|
|
248
|
+
fighters(opts = {}) {
|
|
249
|
+
const { limit, ...params } = opts;
|
|
250
|
+
return this.#paginate("fighters", params, { limit });
|
|
251
|
+
}
|
|
252
|
+
/** Bio, records, career stats, Power Index and CC-licensed images. */
|
|
253
|
+
fighter(idOrSlug) {
|
|
254
|
+
return this.get(`fighters/${encodeURIComponent(String(idOrSlug))}`);
|
|
255
|
+
}
|
|
256
|
+
/** Complete multi-promotion career timeline. */
|
|
257
|
+
fighterHistory(idOrSlug) {
|
|
258
|
+
return this.get(`fighters/${encodeURIComponent(String(idOrSlug))}/history`);
|
|
259
|
+
}
|
|
260
|
+
/** Career statistics, one source-stamped row per scope (`pro-mma`, `ufc-only` …). */
|
|
261
|
+
fighterStats(idOrSlug) {
|
|
262
|
+
return this.get(`fighters/${encodeURIComponent(String(idOrSlug))}/stats`);
|
|
263
|
+
}
|
|
264
|
+
/** Every official ranking row the fighter ever held, newest first. */
|
|
265
|
+
fighterRankings(idOrSlug) {
|
|
266
|
+
return this.get(`fighters/${encodeURIComponent(String(idOrSlug))}/rankings`);
|
|
267
|
+
}
|
|
268
|
+
fighterPowerIndex(idOrSlug) {
|
|
269
|
+
return this.get(
|
|
270
|
+
`fighters/${encodeURIComponent(String(idOrSlug))}/power-index`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
/* ------------------------------------------------------------ rankings */
|
|
274
|
+
/**
|
|
275
|
+
* Official board, point-in-time. `date: 'YYYY-MM-DD'` returns the board
|
|
276
|
+
* that was valid on that day (UFC history back to 2013; rank 0 = champion).
|
|
277
|
+
*/
|
|
278
|
+
rankings(org = "ufc", opts = {}) {
|
|
279
|
+
return this.get(`rankings/${encodeURIComponent(org)}`, opts);
|
|
280
|
+
}
|
|
281
|
+
divisionRankings(org, division, opts = {}) {
|
|
282
|
+
return this.get(
|
|
283
|
+
`rankings/${encodeURIComponent(org)}/${encodeURIComponent(division)}`,
|
|
284
|
+
opts
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
/** Current champions across every launch org. */
|
|
288
|
+
champions() {
|
|
289
|
+
return this.get("champions");
|
|
290
|
+
}
|
|
291
|
+
/** UFCalendar Power Index board. */
|
|
292
|
+
powerIndex(org = "ufc") {
|
|
293
|
+
return this.get(`power-index/${encodeURIComponent(org)}`);
|
|
294
|
+
}
|
|
295
|
+
/* ---------------------------------------------------------------- misc */
|
|
296
|
+
/** Model win probabilities for upcoming UFC bouts. */
|
|
297
|
+
predictionsUpcoming() {
|
|
298
|
+
return this.get("predictions/upcoming");
|
|
299
|
+
}
|
|
300
|
+
/** Who airs the promotion, per ISO-2 country. */
|
|
301
|
+
broadcastRights(org = "ufc", opts = {}) {
|
|
302
|
+
return this.get(`broadcast-rights/${encodeURIComponent(org)}`, opts);
|
|
303
|
+
}
|
|
304
|
+
venue(venueId) {
|
|
305
|
+
return this.get(`venues/${encodeURIComponent(String(venueId))}`);
|
|
306
|
+
}
|
|
307
|
+
/** Typeahead across fighters and events. */
|
|
308
|
+
search(q) {
|
|
309
|
+
return this.get("search", { q });
|
|
310
|
+
}
|
|
311
|
+
/** Your key's month-to-date quota usage. */
|
|
312
|
+
usage() {
|
|
313
|
+
return this.get("usage");
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Subscribable ICS feed URL for calendar apps (authenticates via `?key=`).
|
|
317
|
+
*
|
|
318
|
+
* Throws without a key rather than returning `?key=`: that empty URL is
|
|
319
|
+
* pasted into a calendar app, fails there hours later, and the failure
|
|
320
|
+
* surfaces nowhere near this call.
|
|
321
|
+
*/
|
|
322
|
+
calendarIcsUrl(org = "ufc") {
|
|
323
|
+
if (!this.apiKey) {
|
|
324
|
+
throw new FightAPIError(
|
|
325
|
+
401,
|
|
326
|
+
"no_api_key",
|
|
327
|
+
'No API key. Pass new FightAPI("ufcalendar_...") or set UFCALENDAR_API_KEY. Keys (and the free 1-day trial) live at https://www.ufcalendar.com/account/api'
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
return `${this.baseUrl}/calendar/${encodeURIComponent(org)}.ics?key=${encodeURIComponent(this.apiKey)}`;
|
|
331
|
+
}
|
|
332
|
+
/* ------------------------------------------------------------ webhooks */
|
|
333
|
+
webhookEndpoints() {
|
|
334
|
+
return this.get("webhook-endpoints");
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Register a signed webhook (Pro and up). `events` ⊆ `event.announced`,
|
|
338
|
+
* `fight.result`, `card.changed`, `event.completed`. The signing secret is
|
|
339
|
+
* returned ONCE, in this response.
|
|
340
|
+
*/
|
|
341
|
+
async createWebhookEndpoint(url, events) {
|
|
342
|
+
const body = { url };
|
|
343
|
+
if (events?.length) body.events = events;
|
|
344
|
+
return (await this.#request("POST", "webhook-endpoints", { body })).data;
|
|
345
|
+
}
|
|
346
|
+
async deleteWebhookEndpoint(endpointId) {
|
|
347
|
+
await this.#request("DELETE", `webhook-endpoints/${encodeURIComponent(String(endpointId))}`);
|
|
348
|
+
}
|
|
349
|
+
async rotateWebhookSecret(endpointId) {
|
|
350
|
+
return (await this.#request(
|
|
351
|
+
"POST",
|
|
352
|
+
`webhook-endpoints/${encodeURIComponent(String(endpointId))}/rotate-secret`
|
|
353
|
+
)).data;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
357
|
+
0 && (module.exports = {
|
|
358
|
+
DEFAULT_BASE_URL,
|
|
359
|
+
FightAPI,
|
|
360
|
+
FightAPIError,
|
|
361
|
+
VERSION
|
|
362
|
+
});
|
|
363
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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.1.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} 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 /* -------------------------------------------------------------- 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":[]}
|