vairified 0.1.0 → 0.2.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/dist/index.cjs CHANGED
@@ -22,18 +22,20 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AuthenticationError: () => AuthenticationError,
24
24
  DEFAULT_SCOPES: () => DEFAULT_SCOPES,
25
- Match: () => Match,
26
- MatchResult: () => MatchResult,
25
+ ENVIRONMENTS: () => ENVIRONMENTS,
26
+ LeaderboardResource: () => LeaderboardResource,
27
+ MatchBatchResult: () => MatchBatchResult,
28
+ MatchesResource: () => MatchesResource,
27
29
  Member: () => Member,
30
+ MemberSportMap: () => MemberSportMap,
31
+ MembersResource: () => MembersResource,
28
32
  NotFoundError: () => NotFoundError,
29
33
  OAuthError: () => OAuthError,
30
- Player: () => Player,
34
+ OAuthResource: () => OAuthResource,
31
35
  RateLimitError: () => RateLimitError,
32
- RatingSplit: () => RatingSplit,
33
- RatingSplits: () => RatingSplits,
34
36
  RatingUpdate: () => RatingUpdate,
35
37
  SCOPES: () => SCOPES,
36
- SearchResults: () => SearchResults,
38
+ SportRating: () => SportRating,
37
39
  Vairified: () => Vairified,
38
40
  VairifiedError: () => VairifiedError,
39
41
  ValidationError: () => ValidationError,
@@ -47,9 +49,9 @@ module.exports = __toCommonJS(index_exports);
47
49
 
48
50
  // src/errors.ts
49
51
  var VairifiedError = class extends Error {
50
- /** HTTP status code */
52
+ /** HTTP status code (if the error came from an API response). */
51
53
  statusCode;
52
- /** Response body */
54
+ /** Raw response body parsed as JSON when available. */
53
55
  response;
54
56
  constructor(message, statusCode, response) {
55
57
  super(message);
@@ -59,7 +61,7 @@ var VairifiedError = class extends Error {
59
61
  }
60
62
  };
61
63
  var RateLimitError = class extends VairifiedError {
62
- /** Seconds to wait before retrying */
64
+ /** Seconds to wait before retrying, or `undefined` if the server didn't say. */
63
65
  retryAfter;
64
66
  constructor(message = "Rate limit exceeded", retryAfter, response) {
65
67
  super(message, 429, response);
@@ -86,7 +88,10 @@ var ValidationError = class extends VairifiedError {
86
88
  }
87
89
  };
88
90
  var OAuthError = class extends VairifiedError {
89
- /** OAuth error code (e.g., 'invalid_grant', 'expired_token') */
91
+ /**
92
+ * OAuth error code such as `'invalid_grant'`, `'invalid_scope'`,
93
+ * or `'expired_token'`. Check this to branch on the specific failure.
94
+ */
90
95
  errorCode;
91
96
  constructor(message = "OAuth error", errorCode, response) {
92
97
  super(message, void 0, response);
@@ -95,941 +100,883 @@ var OAuthError = class extends VairifiedError {
95
100
  }
96
101
  };
97
102
 
98
- // src/models.ts
99
- var RatingSplit = class {
100
- /** The rating value */
101
- rating;
102
- /** Abbreviation (e.g., "VG", "50+") */
103
- abbr;
104
- /** Date of last match in this category */
105
- datePlayed;
106
- constructor(data) {
107
- if (typeof data === "number") {
108
- this.rating = data;
109
- this.abbr = "";
110
- } else {
111
- const ratingVal = data.rating;
112
- this.rating = typeof ratingVal === "string" ? Number.parseFloat(ratingVal) || 0 : ratingVal;
113
- this.abbr = data.abbr;
114
- this.datePlayed = data.date_played;
103
+ // src/http.ts
104
+ var HttpTransport = class {
105
+ #config;
106
+ constructor(config) {
107
+ this.#config = config;
108
+ }
109
+ async request(options) {
110
+ const url = buildUrl(this.#config.baseUrl, options.path, options.query);
111
+ const controller = new AbortController();
112
+ const timeoutId = setTimeout(
113
+ () => controller.abort(new Error(`Request timed out after ${this.#config.timeoutMs}ms`)),
114
+ this.#config.timeoutMs
115
+ );
116
+ const headers = {
117
+ "X-API-Key": this.#config.apiKey,
118
+ Accept: "application/json"
119
+ };
120
+ const init = {
121
+ method: options.method,
122
+ headers,
123
+ signal: controller.signal
124
+ };
125
+ if (options.body !== void 0) {
126
+ headers["Content-Type"] = "application/json";
127
+ init.body = JSON.stringify(options.body);
115
128
  }
116
- }
117
- };
118
- var RatingSplits = class {
119
- /** Map of category names to rating splits */
120
- splits;
121
- constructor(data) {
122
- this.splits = /* @__PURE__ */ new Map();
123
- if (data) {
124
- for (const [key, value] of Object.entries(data)) {
125
- this.splits.set(key, new RatingSplit(value));
126
- }
129
+ let response;
130
+ try {
131
+ response = await this.#config.fetch(url, init);
132
+ } finally {
133
+ clearTimeout(timeoutId);
127
134
  }
128
- }
129
- /** Get rating for a category */
130
- get(category) {
131
- return this.splits.get(category)?.rating;
132
- }
133
- /** Open division rating */
134
- get open() {
135
- return this.get("open") ?? this.get("VO");
136
- }
137
- /** Gender-specific rating (same gender doubles) */
138
- get gender() {
139
- return this.get("gender") ?? this.get("VG");
140
- }
141
- /** Mixed doubles rating */
142
- get mixed() {
143
- return this.get("mixed") ?? this.get("VM");
144
- }
145
- /** Recreational rating */
146
- get recreational() {
147
- return this.get("recreational") ?? this.get("R");
148
- }
149
- /** Singles rating */
150
- get singles() {
151
- return this.get("singles") ?? this.get("S");
152
- }
153
- /** Best available verified rating */
154
- get best() {
155
- const ratings = Array.from(this.splits.values()).map((s) => s.rating).filter((r) => r > 0);
156
- return ratings.length > 0 ? Math.max(...ratings) : void 0;
157
- }
158
- /** Convert to plain object */
159
- toJSON() {
160
- const result = {};
161
- for (const [key, split] of this.splits) {
162
- result[key] = { rating: split.rating, abbr: split.abbr };
135
+ if (!response.ok) {
136
+ await throwFromResponse(response);
163
137
  }
164
- return result;
165
- }
166
- };
167
- function isSearchData(data) {
168
- return "displayName" in data;
169
- }
170
- var Player = class {
171
- /** External player ID (vair_mem_xxx format) */
172
- id;
173
- /** Display name (First Name + Last Initial from search) */
174
- displayName;
175
- /** First name (only from connected member) */
176
- firstName;
177
- /** Last name (only from connected member) */
178
- lastName;
179
- /** Primary/overall rating (2.0-8.0) */
180
- rating;
181
- /** Whether player is verified */
182
- isVairified;
183
- /** Whether player has connected to your app */
184
- isConnected;
185
- /** Ratings by category (only from connected member) */
186
- ratingSplits;
187
- /** City */
188
- city;
189
- /** State code */
190
- state;
191
- /** Country code */
192
- country;
193
- _client;
194
- constructor(data, client) {
195
- if (isSearchData(data)) {
196
- this.id = data.id;
197
- this.displayName = data.displayName;
198
- this.rating = data.rating ?? 0;
199
- this.isVairified = data.isVairified ?? false;
200
- this.isConnected = data.isConnected ?? false;
201
- this.ratingSplits = new RatingSplits();
202
- } else {
203
- this.id = data.id;
204
- this.firstName = data.firstName ?? "";
205
- this.lastName = data.lastName ?? "";
206
- this.rating = data.rating ?? 0;
207
- this.isVairified = data.isVairified ?? false;
208
- this.isConnected = true;
209
- this.ratingSplits = new RatingSplits(data.ratingSplits);
138
+ if (response.status === 204 || response.headers.get("content-length") === "0") {
139
+ return void 0;
210
140
  }
211
- this.city = data.city;
212
- this.state = data.state;
213
- this.country = data.country;
214
- this._client = client;
215
- }
216
- /** Full name (or display name if full name not available) */
217
- get name() {
218
- if (this.firstName && this.lastName) {
219
- return `${this.firstName} ${this.lastName}`.trim();
141
+ const text = await response.text();
142
+ if (text.length === 0) {
143
+ return void 0;
144
+ }
145
+ try {
146
+ return JSON.parse(text);
147
+ } catch {
148
+ throw new VairifiedError(`Unable to parse response as JSON: ${text}`, response.status);
220
149
  }
221
- return this.displayName ?? "";
222
- }
223
- /** Best verified rating */
224
- get verifiedRating() {
225
- return this.ratingSplits.best;
226
- }
227
- toString() {
228
- const verified = this.isVairified ? " \u2713" : "";
229
- return `${this.name} (${this.rating.toFixed(2)})${verified}`;
230
150
  }
231
151
  };
232
- var Member = class extends Player {
233
- /** Email address (only if profile:email scope granted) */
234
- email;
235
- /** Scopes the player granted to your app */
236
- grantedScopes;
237
- constructor(data, client) {
238
- super(data, client);
239
- this.email = data.email;
240
- this.grantedScopes = data.grantedScopes ?? [];
241
- }
242
- /** Check if the player has granted a specific scope */
243
- hasScope(scope) {
244
- return this.grantedScopes.includes(scope);
245
- }
246
- /** Refresh member data from API */
247
- async refresh() {
248
- if (!this._client) {
249
- throw new Error("Member not connected to client");
152
+ function buildUrl(baseUrl, path, query) {
153
+ const cleanBase = baseUrl.replace(/\/+$/, "");
154
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
155
+ const url = new URL(cleanBase + cleanPath);
156
+ if (query) {
157
+ for (const [key, value] of Object.entries(query)) {
158
+ if (value === null || value === void 0) continue;
159
+ if (Array.isArray(value)) {
160
+ if (value.length === 0) continue;
161
+ url.searchParams.set(key, value.join(","));
162
+ } else {
163
+ url.searchParams.set(key, String(value));
164
+ }
250
165
  }
251
- const updated = await this._client.getMember(this.id);
252
- Object.assign(this, updated);
253
- return this;
254
166
  }
255
- };
256
- function generateId() {
257
- return `SDK-${Math.random().toString(36).substring(2, 14)}`;
167
+ return url.toString();
258
168
  }
259
- var Match = class {
260
- /** Event/tournament name */
261
- event;
262
- /** Bracket/division name */
263
- bracket;
264
- /** Match date */
265
- date;
266
- /** Team 1 player IDs */
267
- team1;
268
- /** Team 2 player IDs */
269
- team2;
270
- /** Game scores */
271
- scores;
272
- /** Match type */
273
- matchType;
274
- /** Match source */
275
- source;
276
- /** Location */
277
- location;
278
- /** Unique identifier */
279
- identifier;
280
- /** Match ID (set after submission) */
281
- id;
282
- constructor(data) {
283
- this.event = data.event;
284
- this.bracket = data.bracket;
285
- this.date = data.date instanceof Date ? data.date : new Date(data.date);
286
- this.team1 = data.team1;
287
- this.team2 = data.team2;
288
- this.scores = data.scores;
289
- this.matchType = data.matchType ?? "SIDEOUT";
290
- this.source = data.source ?? "PARTNER";
291
- this.location = data.location;
292
- this.identifier = data.identifier ?? generateId();
293
- }
294
- /** Match format: SINGLES or DOUBLES */
295
- get format() {
296
- return this.team1.length === 1 ? "SINGLES" : "DOUBLES";
297
- }
298
- /** Team that won (1 or 2). Returns 0 if tie. */
299
- get winner() {
300
- let t1Wins = 0;
301
- let t2Wins = 0;
302
- for (const [s1, s2] of this.scores) {
303
- if (s1 > s2) t1Wins++;
304
- else if (s2 > s1) t2Wins++;
305
- }
306
- if (t1Wins > t2Wins) return 1;
307
- if (t2Wins > t1Wins) return 2;
308
- return 0;
309
- }
310
- /** Score summary like "11-9, 11-7" */
311
- get scoreSummary() {
312
- return this.scores.map(([s1, s2]) => `${s1}-${s2}`).join(", ");
313
- }
314
- /** Convert to API request format */
315
- toJSON() {
316
- const player1A = this.team1[0];
317
- const player1B = this.team2[0];
318
- if (!player1A || !player1B) {
319
- throw new Error("Match must have at least one player per team");
169
+ async function throwFromResponse(response) {
170
+ const status = response.status;
171
+ const text = await response.text().catch(() => "");
172
+ let body = null;
173
+ if (text.length > 0) {
174
+ try {
175
+ body = JSON.parse(text);
176
+ } catch {
177
+ body = null;
320
178
  }
321
- const teamA = { player1: player1A };
322
- const teamB = { player1: player1B };
323
- if (this.team1[1]) teamA.player2 = this.team1[1];
324
- if (this.team2[1]) teamB.player2 = this.team2[1];
325
- const gameKeys = ["game1", "game2", "game3", "game4", "game5"];
326
- for (let i = 0; i < Math.min(this.scores.length, 5); i++) {
327
- const score = this.scores[i];
328
- const key = gameKeys[i];
329
- if (score && key) {
330
- teamA[key] = score[0];
331
- teamB[key] = score[1];
332
- }
179
+ }
180
+ let message;
181
+ if (body && typeof body === "object" && !Array.isArray(body)) {
182
+ const apiBody = body;
183
+ message = apiBody.message || apiBody.error || text || `HTTP ${status}`;
184
+ } else {
185
+ message = text || `HTTP ${status}`;
186
+ }
187
+ switch (status) {
188
+ case 400:
189
+ throw new ValidationError(message, body);
190
+ case 401:
191
+ throw new AuthenticationError(message, body);
192
+ case 404:
193
+ throw new NotFoundError(message, body);
194
+ case 429: {
195
+ const retryAfterHeader = response.headers.get("Retry-After");
196
+ const retryAfter = retryAfterHeader ? Number.parseInt(retryAfterHeader, 10) : void 0;
197
+ throw new RateLimitError(message, Number.isFinite(retryAfter) ? retryAfter : void 0, body);
333
198
  }
334
- return {
335
- identifier: this.identifier,
336
- bracket: this.bracket,
337
- event: this.event,
338
- format: this.format,
339
- matchDate: this.date.toISOString(),
340
- matchSource: this.source,
341
- matchType: this.matchType,
342
- location: this.location,
343
- teamA,
344
- teamB
199
+ default:
200
+ throw new VairifiedError(message, status, body);
201
+ }
202
+ }
203
+
204
+ // src/resources/leaderboard.ts
205
+ var LeaderboardResource = class {
206
+ #http;
207
+ /** @internal */
208
+ constructor(http) {
209
+ this.#http = http;
210
+ }
211
+ /** Fetch a leaderboard page with optional filters. */
212
+ async list(options = {}) {
213
+ const query = {
214
+ limit: options.limit ?? 50,
215
+ offset: options.offset ?? 0,
216
+ category: options.category,
217
+ ageBracket: options.ageBracket,
218
+ scope: options.scope,
219
+ state: options.state,
220
+ city: options.city,
221
+ clubId: options.clubId,
222
+ gender: options.gender?.toUpperCase(),
223
+ minGames: options.minGames,
224
+ search: options.search,
225
+ verifiedOnly: options.verifiedOnly === true ? true : void 0
226
+ };
227
+ const data = await this.#http.request({
228
+ method: "GET",
229
+ path: "/leaderboard",
230
+ query
231
+ });
232
+ return data ?? {};
233
+ }
234
+ /** Fetch a specific player's rank plus nearby players. */
235
+ async rank(playerId, options = {}) {
236
+ const body = {
237
+ playerId,
238
+ category: options.category ?? "doubles",
239
+ ageBracket: options.ageBracket ?? "open",
240
+ scope: options.scope ?? "global",
241
+ contextSize: options.contextSize ?? 5
345
242
  };
243
+ if (options.state !== void 0) body.state = options.state;
244
+ if (options.city !== void 0) body.city = options.city;
245
+ if (options.clubId !== void 0) body.clubId = options.clubId;
246
+ const data = await this.#http.request({
247
+ method: "POST",
248
+ path: "/leaderboard/rank",
249
+ body
250
+ });
251
+ return data ?? {};
252
+ }
253
+ /** List available leaderboard categories, brackets, and scopes. */
254
+ async categories() {
255
+ const data = await this.#http.request({
256
+ method: "GET",
257
+ path: "/leaderboard/categories"
258
+ });
259
+ return data ?? {};
346
260
  }
347
261
  };
348
- var MatchResult = class {
349
- /** Whether submission succeeded */
262
+
263
+ // src/models/match-batch-result.ts
264
+ var MatchBatchResult = class {
350
265
  success;
351
- /** Number of matches processed */
352
266
  numMatches;
353
- /** Number of games recorded */
354
267
  numGames;
355
- /** Whether this was a dry-run (validation only) */
356
268
  dryRun;
357
- /** Human-readable result message */
358
269
  message;
359
- /** List of validation/processing errors */
360
270
  errors;
361
- constructor(data) {
362
- this.success = data.success;
363
- this.numMatches = data.numMatches;
364
- this.numGames = data.numGames;
365
- this.dryRun = data.dryRun ?? false;
366
- this.message = data.message;
367
- this.errors = data.errors ?? [];
368
- }
369
- /** Alias for dryRun */
370
- get isDryRun() {
371
- return this.dryRun;
372
- }
373
- /** Returns true if submission succeeded without errors */
271
+ constructor(wire) {
272
+ this.success = wire.success;
273
+ this.numMatches = wire.numMatches;
274
+ this.numGames = wire.numGames;
275
+ this.dryRun = wire.dryRun ?? null;
276
+ this.message = wire.message ?? null;
277
+ this.errors = wire.errors ? Object.freeze([...wire.errors]) : null;
278
+ Object.freeze(this);
279
+ }
280
+ /** Shorthand: successful submission with zero errors. */
374
281
  get ok() {
375
- return this.success && this.errors.length === 0;
282
+ return this.success && (this.errors === null || this.errors.length === 0);
376
283
  }
377
- };
378
- var RatingUpdate = class {
379
- /** External player ID (vair_mem_xxx format) */
380
- id;
381
- /** Member name */
382
- memberName;
383
- /** Previous rating */
384
- previousRating;
385
- /** New rating */
386
- newRating;
387
- /** When the change occurred */
388
- changedAt;
389
- /** Updated rating splits */
390
- ratingSplits;
391
- _client;
392
- constructor(data, client) {
393
- this.id = data.id;
394
- this.memberName = data.memberName;
395
- this.previousRating = data.previousRating ?? 0;
396
- this.newRating = data.newRating ?? 0;
397
- this.changedAt = data.changedAt ? new Date(data.changedAt) : /* @__PURE__ */ new Date();
398
- this.ratingSplits = new RatingSplits(data.ratingSplits);
399
- this._client = client;
400
- }
401
- /** Amount of rating change */
402
- get change() {
403
- return this.newRating - this.previousRating;
404
- }
405
- /** Whether rating improved */
406
- get improved() {
407
- return this.change > 0;
408
- }
409
- /** Fetch the member associated with this update */
410
- async getMember() {
411
- if (!this._client) {
412
- throw new Error("Update not connected to client");
413
- }
414
- return this._client.getMember(this.id);
284
+ /** Whether this was a dry-run (validation only, nothing persisted). */
285
+ get isDryRun() {
286
+ return this.dryRun === true;
415
287
  }
416
288
  toString() {
417
- const direction = this.improved ? "\u2191" : "\u2193";
418
- const name = this.memberName ? ` (${this.memberName})` : "";
419
- return `${this.id}${name}: ${this.previousRating.toFixed(2)} ${direction} ${this.newRating.toFixed(2)}`;
289
+ const mode = this.isDryRun ? " [dry-run]" : "";
290
+ const errs = this.errors && this.errors.length > 0 ? ` errors=${this.errors.length}` : "";
291
+ const status = this.ok ? "ok" : "FAILED";
292
+ return `MatchBatchResult ${status}${mode} matches=${this.numMatches} games=${this.numGames}${errs}`;
420
293
  }
421
294
  };
422
- var SearchResults = class {
423
- /** List of players */
424
- players;
425
- /** Total matching players */
426
- total;
427
- /** Current page */
428
- page;
429
- /** Results per page */
430
- limit;
431
- _client;
432
- _filters;
433
- constructor(data, client, filters = {}) {
434
- this.players = data.players.map((p) => new Player(p, client));
435
- this.total = data.total;
436
- this.page = data.page;
437
- this.limit = data.limit;
438
- this._client = client;
439
- this._filters = filters;
440
- }
441
- /** Whether more results are available */
442
- get hasMore() {
443
- return this.page * this.limit < this.total;
444
- }
445
- /** Total number of pages */
446
- get pages() {
447
- return this.limit > 0 ? Math.ceil(this.total / this.limit) : 0;
448
- }
449
- /** Number of players in current page */
450
- get length() {
451
- return this.players.length;
452
- }
453
- /** Get player by index */
454
- at(index) {
455
- return this.players[index];
456
- }
457
- /** Iterate over players */
458
- [Symbol.iterator]() {
459
- return this.players[Symbol.iterator]();
295
+
296
+ // src/resources/matches.ts
297
+ var MatchesResource = class {
298
+ #http;
299
+ /** @internal */
300
+ constructor(http) {
301
+ this.#http = http;
460
302
  }
461
- /** Fetch next page of results */
462
- async nextPage() {
463
- if (!this._client) {
464
- throw new Error("Results not connected to client");
465
- }
466
- if (!this.hasMore) {
467
- throw new Error("No more pages");
468
- }
469
- return this._client.search({
470
- ...this._filters,
471
- page: this.page + 1
303
+ /**
304
+ * Submit a {@link MatchBatch} for rating calculation.
305
+ *
306
+ * All players in every match must have granted the `match:submit`
307
+ * scope via OAuth (unless your API key has the
308
+ * `match:submit:trusted` scope, which skips per-player consent).
309
+ *
310
+ * Set `batch.dryRun = true` to validate without persisting.
311
+ *
312
+ * ```ts
313
+ * const result = await client.matches.submit({
314
+ * sport: 'pickleball',
315
+ * winScore: 11,
316
+ * winBy: 2,
317
+ * bracket: '4.0 Doubles',
318
+ * event: 'Weekly League',
319
+ * matchDate: '2026-04-11T14:00:00Z',
320
+ * matches: [
321
+ * {
322
+ * identifier: 'm1',
323
+ * teams: [['vair_mem_aaa', 'vair_mem_bbb'],
324
+ * ['vair_mem_ccc', 'vair_mem_ddd']],
325
+ * games: [{ scores: [11, 8] }, { scores: [11, 5] }],
326
+ * },
327
+ * ],
328
+ * });
329
+ * if (result.ok) {
330
+ * console.log(`Submitted ${result.numGames} games`);
331
+ * }
332
+ * ```
333
+ */
334
+ async submit(batch) {
335
+ const wire = await this.#http.request({
336
+ method: "POST",
337
+ path: "/partner/matches",
338
+ body: batch
472
339
  });
340
+ return new MatchBatchResult(wire);
341
+ }
342
+ /** Send a test payload to a webhook URL. */
343
+ async testWebhook(webhookUrl) {
344
+ const data = await this.#http.request({
345
+ method: "POST",
346
+ path: "/partner/webhook-test",
347
+ body: { webhookUrl }
348
+ });
349
+ return data ?? {};
473
350
  }
474
351
  };
475
352
 
476
- // src/oauth.ts
477
- var SCOPES = {
478
- "profile:read": "Access your name, location, and verification status",
479
- "profile:email": "Access your email address",
480
- "rating:read": "View your current rating and rating splits",
481
- "rating:history": "View your complete rating history",
482
- "match:submit": "Submit match results on your behalf",
483
- "webhook:subscribe": "Receive notifications when your rating changes"
484
- };
485
- var DEFAULT_SCOPES = ["profile:read", "rating:read"];
486
- function getAuthorizationUrl(config, scopes = DEFAULT_SCOPES, state) {
487
- const baseUrl = config.baseUrl || "https://api-next.vairified.com/api/v1";
488
- const scopeSet = new Set(scopes);
489
- scopeSet.add("profile:read");
490
- const scopeList = Array.from(scopeSet);
491
- const params = new URLSearchParams({
492
- redirect_uri: config.redirectUri,
493
- scope: scopeList.join(","),
494
- response_type: "code"
495
- });
496
- if (state) {
497
- params.set("state", state);
353
+ // src/models/sport-rating.ts
354
+ var SportRating = class {
355
+ /** Primary rating for this sport. */
356
+ rating;
357
+ /** Category abbreviation for the primary rating (e.g. `'VO'`). */
358
+ abbr;
359
+ #splits;
360
+ constructor(wire) {
361
+ this.rating = wire.rating;
362
+ this.abbr = wire.abbr;
363
+ this.#splits = new Map(Object.entries(wire.ratingSplits ?? {}));
364
+ Object.freeze(this);
498
365
  }
499
- return `${baseUrl}/partner/oauth/authorize?${params.toString()}`;
500
- }
501
- function validateScope(scope) {
502
- return scope in SCOPES;
503
- }
504
- function describeScope(scope) {
505
- return SCOPES[scope] ?? `Unknown scope: ${scope}`;
506
- }
507
- function describeScopes(scopes) {
508
- return scopes.map((scope) => ({
509
- scope,
510
- description: describeScope(scope)
511
- }));
512
- }
513
- function generateState() {
514
- const array = new Uint8Array(16);
515
- if (typeof crypto !== "undefined" && crypto.getRandomValues) {
516
- crypto.getRandomValues(array);
517
- } else {
518
- for (let i = 0; i < array.length; i++) {
519
- array[i] = Math.floor(Math.random() * 256);
520
- }
366
+ /**
367
+ * Look up a rating split by key (e.g. `'overall-open'`,
368
+ * `'singles-12-13'`, `'gender-40+'`). Returns `undefined` if the
369
+ * player has no rating for that bracket.
370
+ */
371
+ get(key) {
372
+ return this.#splits.get(key);
373
+ }
374
+ /** Whether the player has a rating for the given split key. */
375
+ has(key) {
376
+ return this.#splits.has(key);
377
+ }
378
+ /** Number of rating splits. */
379
+ get size() {
380
+ return this.#splits.size;
381
+ }
382
+ /** All split keys the player has ratings for. */
383
+ keys() {
384
+ return this.#splits.keys();
385
+ }
386
+ /** All rating splits the player has. */
387
+ values() {
388
+ return this.#splits.values();
389
+ }
390
+ /** `[key, split]` pairs for every rating split. */
391
+ entries() {
392
+ return this.#splits.entries();
393
+ }
394
+ /**
395
+ * `for (const [key, split] of sportRating) { ... }` — iterate every
396
+ * rating split the player has in this sport.
397
+ */
398
+ [Symbol.iterator]() {
399
+ return this.#splits.entries();
521
400
  }
522
- return Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join("");
523
- }
524
-
525
- // src/client.ts
526
- var ENVIRONMENTS = {
527
- production: "https://api-next.vairified.com/api/v1",
528
- staging: "https://api-staging.vairified.com/api/v1",
529
- local: "http://localhost:3001/api/v1"
530
401
  };
531
- var DEFAULT_BASE_URL = ENVIRONMENTS.production;
532
- var DEFAULT_TIMEOUT = 3e4;
533
- var Vairified = class {
534
- /** API key */
535
- apiKey;
536
- /** Base URL */
537
- baseUrl;
538
- /** Environment name */
539
- env;
540
- /** Request timeout in ms */
541
- timeout;
542
- constructor(options = {}) {
543
- this.apiKey = options.apiKey || this.getEnvApiKey();
544
- if (!this.apiKey) {
545
- throw new Error("API key required. Pass apiKey option or set VAIRIFIED_API_KEY env var.");
546
- }
547
- if (options.baseUrl) {
548
- this.baseUrl = options.baseUrl.replace(/\/$/, "");
549
- this.env = "production";
550
- } else if (options.env) {
551
- this.baseUrl = ENVIRONMENTS[options.env];
552
- this.env = options.env;
553
- } else {
554
- const envVar = this.getEnvVar("VAIRIFIED_ENV");
555
- const defaultEnv = envVar && envVar in ENVIRONMENTS ? envVar : "production";
556
- this.baseUrl = ENVIRONMENTS[defaultEnv] || DEFAULT_BASE_URL;
557
- this.env = defaultEnv;
558
- }
559
- this.timeout = options.timeout || DEFAULT_TIMEOUT;
402
+
403
+ // src/models/member.ts
404
+ var MemberSportMap = class {
405
+ #sports;
406
+ constructor(wire) {
407
+ const entries = Object.entries(wire ?? {}).map(
408
+ ([code, w]) => [code, new SportRating(w)]
409
+ );
410
+ this.#sports = new Map(entries);
411
+ Object.freeze(this);
560
412
  }
561
- getEnvVar(name) {
562
- if (typeof process !== "undefined" && process.env?.[name]) {
563
- return process.env[name];
564
- }
565
- return "";
413
+ get(sport) {
414
+ return this.#sports.get(sport);
566
415
  }
567
- getEnvApiKey() {
568
- return this.getEnvVar("VAIRIFIED_API_KEY");
416
+ has(sport) {
417
+ return this.#sports.has(sport);
569
418
  }
570
- getHeaders() {
571
- return {
572
- "X-API-Key": this.apiKey,
573
- "Content-Type": "application/json",
574
- Accept: "application/json"
575
- };
419
+ get size() {
420
+ return this.#sports.size;
576
421
  }
577
- async handleError(response) {
578
- let body;
579
- let message;
580
- try {
581
- body = await response.json();
582
- message = body.message || response.statusText;
583
- } catch {
584
- message = response.statusText;
585
- }
586
- const status = response.status;
587
- if (status === 401) throw new AuthenticationError(message, body);
588
- if (status === 404) throw new NotFoundError(message, body);
589
- if (status === 429) {
590
- const retryAfter = response.headers.get("Retry-After");
591
- throw new RateLimitError(
592
- message,
593
- retryAfter ? Number.parseInt(retryAfter, 10) : void 0,
594
- body
595
- );
596
- }
597
- if (status === 400) throw new ValidationError(message, body);
598
- throw new VairifiedError(message, status, body);
599
- }
600
- async request(method, path, options) {
601
- let url = `${this.baseUrl}${path}`;
602
- if (options?.params) {
603
- const searchParams = new URLSearchParams();
604
- for (const [key, value] of Object.entries(options.params)) {
605
- if (value !== void 0 && value !== null) {
606
- searchParams.append(key, String(value));
607
- }
608
- }
609
- const queryString = searchParams.toString();
610
- if (queryString) url += `?${queryString}`;
611
- }
612
- const controller = new AbortController();
613
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
614
- try {
615
- const response = await fetch(url, {
616
- method,
617
- headers: this.getHeaders(),
618
- body: options?.body ? JSON.stringify(options.body) : void 0,
619
- signal: controller.signal
620
- });
621
- if (!response.ok) await this.handleError(response);
622
- return await response.json();
623
- } finally {
624
- clearTimeout(timeoutId);
625
- }
422
+ keys() {
423
+ return this.#sports.keys();
424
+ }
425
+ values() {
426
+ return this.#sports.values();
427
+ }
428
+ entries() {
429
+ return this.#sports.entries();
430
+ }
431
+ [Symbol.iterator]() {
432
+ return this.#sports.entries();
433
+ }
434
+ };
435
+ var Member = class {
436
+ memberId;
437
+ id;
438
+ firstName;
439
+ lastName;
440
+ fullName;
441
+ displayName;
442
+ age;
443
+ city;
444
+ state;
445
+ zip;
446
+ country;
447
+ gender;
448
+ status;
449
+ sport;
450
+ activeLeagues;
451
+ email;
452
+ grantedScopes;
453
+ constructor(wire) {
454
+ this.memberId = wire.memberId;
455
+ this.id = wire.id ?? null;
456
+ this.firstName = wire.firstName;
457
+ this.lastName = wire.lastName;
458
+ this.fullName = wire.fullName;
459
+ this.displayName = wire.displayName;
460
+ this.age = wire.age ?? null;
461
+ this.city = wire.city ?? null;
462
+ this.state = wire.state ?? null;
463
+ this.zip = wire.zip ?? null;
464
+ this.country = wire.country ?? null;
465
+ this.gender = wire.gender ?? null;
466
+ this.status = Object.freeze({ ...wire.status });
467
+ this.sport = new MemberSportMap(wire.sport);
468
+ this.activeLeagues = wire.activeLeagues ? Object.freeze([...wire.activeLeagues]) : null;
469
+ this.email = wire.email ?? null;
470
+ this.grantedScopes = wire.grantedScopes ? Object.freeze([...wire.grantedScopes]) : null;
471
+ Object.freeze(this);
472
+ }
473
+ /** Full name — alias for {@link fullName}, matching common usage. */
474
+ get name() {
475
+ return this.fullName;
476
+ }
477
+ /** The list of sport codes this player has ratings in. */
478
+ get sports() {
479
+ return [...this.sport.keys()];
626
480
  }
627
- // ---------------------------------------------------------------------------
628
- // Member Operations
629
- // ---------------------------------------------------------------------------
630
481
  /**
631
- * Get a connected member by their external ID.
632
- *
633
- * **Requires OAuth Connection**: The player must have connected their
634
- * account to your application via OAuth before you can access their data.
482
+ * Primary rating for a given sport.
635
483
  *
636
- * @param playerId - External player ID (vair_mem_xxx format)
637
- * @returns Member object with profile and rating data
638
- * @throws NotFoundError if member is not found or invalid ID format
639
- * @throws ForbiddenError if player has not connected to your app
640
- *
641
- * @example
642
- * ```ts
643
- * const member = await client.getMember('vair_mem_0ABC123def456GHI789jk');
644
- * console.log(member.name, member.rating);
645
- * console.log(member.ratingSplits.open); // Open division rating
646
- * console.log(member.grantedScopes); // ['profile:read', 'rating:read']
647
- * ```
484
+ * @param sport Sport code defaults to `'pickleball'`.
485
+ * @returns The primary rating value, or `null` if the player has no
486
+ * ratings for that sport.
648
487
  */
649
- async getMember(playerId) {
650
- const data = await this.request("GET", "/partner/member", {
651
- params: { id: playerId }
652
- });
653
- return new Member(data, this);
488
+ ratingFor(sport = "pickleball") {
489
+ return this.sport.get(sport)?.rating ?? null;
654
490
  }
655
- // ---------------------------------------------------------------------------
656
- // Search Operations
657
- // ---------------------------------------------------------------------------
658
491
  /**
659
- * Search for players.
660
- *
661
- * @param filters - Search filters
662
- * @returns SearchResults with players and pagination
492
+ * Get a specific rating split for a sport.
663
493
  *
664
- * @example
665
- * ```ts
666
- * const results = await client.search({
667
- * city: 'Austin',
668
- * ratingMin: 4.0,
669
- * vairifiedOnly: true,
670
- * });
671
- *
672
- * for (const player of results) {
673
- * console.log(player.name, player.rating);
674
- * }
675
- *
676
- * // Pagination
677
- * if (results.hasMore) {
678
- * const nextPage = await results.nextPage();
679
- * }
680
- * ```
494
+ * @param key Split key (e.g. `'overall-open'`).
495
+ * @param sport Sport code — defaults to `'pickleball'`.
681
496
  */
682
- async search(filters = {}) {
683
- const params = {
684
- limit: filters.limit ?? 20
685
- };
686
- if (filters.name) params.member = filters.name;
687
- if (filters.city) params.city = filters.city;
688
- if (filters.state) params.state = filters.state;
689
- if (filters.country) params.country = filters.country;
690
- if (filters.zipCode) params.zip = filters.zipCode;
691
- if (filters.ratingMin !== void 0) params.rating1 = filters.ratingMin;
692
- if (filters.ratingMax !== void 0) params.rating2 = filters.ratingMax;
693
- if (filters.gender) params.gender = filters.gender;
694
- if (filters.vairifiedOnly) params.vairified = true;
695
- if (filters.sortBy) {
696
- params.sortField = filters.sortBy;
697
- params.sortDirection = filters.sortOrder ?? "desc";
698
- }
699
- if (filters.age !== void 0) {
700
- params.ageFilterType = "exact";
701
- params.age1 = filters.age;
702
- } else if (filters.ageMin !== void 0 && filters.ageMax !== void 0) {
703
- params.ageFilterType = "range";
704
- params.age1 = filters.ageMin;
705
- params.age2 = filters.ageMax;
706
- } else if (filters.ageMin !== void 0) {
707
- params.ageFilterType = "above";
708
- params.age1 = filters.ageMin;
709
- } else if (filters.ageMax !== void 0) {
710
- params.ageFilterType = "below";
711
- params.age1 = filters.ageMax;
712
- }
713
- const page = filters.page ?? 1;
714
- if (page > 1) {
715
- params.offset = (page - 1) * (filters.limit ?? 20);
497
+ split(key, sport = "pickleball") {
498
+ return this.sport.get(sport)?.get(key) ?? null;
499
+ }
500
+ /** Compact summary for console output. */
501
+ toString() {
502
+ const primary = this.sport.values().next().value;
503
+ if (primary) {
504
+ return `Member #${this.memberId} '${this.displayName}' rating=${primary.rating.toFixed(
505
+ 3
506
+ )} ${primary.abbr}`;
716
507
  }
717
- const data = await this.request(
718
- "GET",
719
- "/partner/search",
720
- {
721
- params
722
- }
723
- );
724
- const normalized = Array.isArray(data) ? { players: data, total: data.length, page, limit: filters.limit ?? 20 } : data;
725
- return new SearchResults(normalized, this, filters);
508
+ return `Member #${this.memberId} '${this.displayName}'`;
509
+ }
510
+ };
511
+
512
+ // src/models/rating-update.ts
513
+ var RatingUpdate = class {
514
+ memberId;
515
+ id;
516
+ displayName;
517
+ sport;
518
+ previousRating;
519
+ newRating;
520
+ changedAt;
521
+ ratingSplits;
522
+ constructor(wire) {
523
+ this.memberId = wire.memberId;
524
+ this.id = wire.id ?? null;
525
+ this.displayName = wire.displayName ?? null;
526
+ this.sport = wire.sport ?? null;
527
+ this.previousRating = wire.previousRating ?? null;
528
+ this.newRating = wire.newRating ?? null;
529
+ this.changedAt = wire.changedAt ?? null;
530
+ this.ratingSplits = wire.ratingSplits ? Object.freeze({ ...wire.ratingSplits }) : null;
531
+ Object.freeze(this);
726
532
  }
727
533
  /**
728
- * Find a single player by name.
729
- *
730
- * @param name - Player name to search for
731
- * @returns Player if found, undefined otherwise
732
- *
733
- * @example
734
- * ```ts
735
- * const player = await client.findPlayer('John Smith');
736
- * if (player) {
737
- * console.log(player.rating);
738
- * }
739
- * ```
534
+ * Rating change amount `newRating - previousRating`. Returns `null`
535
+ * if either rating is missing from the update payload.
740
536
  */
741
- async findPlayer(name) {
742
- const results = await this.search({ name, limit: 1 });
743
- return results.at(0);
537
+ get delta() {
538
+ if (this.previousRating === null || this.newRating === null) {
539
+ return null;
540
+ }
541
+ return this.newRating - this.previousRating;
542
+ }
543
+ /** `true` when the new rating is strictly higher than the previous. */
544
+ get improved() {
545
+ const delta = this.delta;
546
+ return delta !== null && delta > 0;
547
+ }
548
+ toString() {
549
+ const arrow = this.improved ? "\u2191" : "\u2193";
550
+ const prev = this.previousRating !== null ? this.previousRating.toFixed(3) : "?";
551
+ const next = this.newRating !== null ? this.newRating.toFixed(3) : "?";
552
+ const name = this.displayName ? ` '${this.displayName}'` : "";
553
+ return `RatingUpdate #${this.memberId}${name} ${prev} ${arrow} ${next}`;
554
+ }
555
+ };
556
+
557
+ // src/resources/members.ts
558
+ var DEFAULT_PAGE_SIZE = 20;
559
+ var MAX_PAGE_SIZE = 100;
560
+ var MembersResource = class {
561
+ #http;
562
+ /** @internal */
563
+ constructor(http) {
564
+ this.#http = http;
744
565
  }
745
- // ---------------------------------------------------------------------------
746
- // Match Operations
747
- // ---------------------------------------------------------------------------
748
566
  /**
749
- * Submit a single match.
567
+ * Get a connected member by external ID.
750
568
  *
751
- * @param match - Match object with teams and scores
752
- * @returns MatchResult with submission status
569
+ * **Requires an active OAuth connection** between your partner app
570
+ * and the player. Use the OAuth flow on `client.oauth` first.
571
+ *
572
+ * @param playerId External player ID in `vair_mem_xxx` format.
573
+ * @param options.sport Optional sport filter — single code or list.
574
+ * When omitted, the response contains every sport the player has
575
+ * ratings in.
576
+ * @throws {@link NotFoundError} if the external ID is unknown.
577
+ * @throws {@link VairifiedError} if the player has not connected to
578
+ * your app (403) or the request otherwise fails.
753
579
  *
754
580
  * @example
755
581
  * ```ts
756
- * const match = new Match({
757
- * event: 'Weekly League',
758
- * bracket: '4.0 Doubles',
759
- * date: new Date(),
760
- * team1: ['p1', 'p2'],
761
- * team2: ['p3', 'p4'],
762
- * scores: [[11, 9], [11, 7]],
763
- * });
582
+ * const member = await client.members.get('vair_mem_xxx');
583
+ * console.log(member.name, member.ratingFor('pickleball'));
764
584
  *
765
- * const result = await client.submitMatch(match);
766
- * if (result.ok) {
767
- * console.log(`Submitted ${result.numGames} games`);
768
- * }
585
+ * // Just pickleball
586
+ * const member2 = await client.members.get('vair_mem_xxx', { sport: 'pickleball' });
587
+ *
588
+ * // Multiple sports
589
+ * const member3 = await client.members.get('vair_mem_xxx', {
590
+ * sport: ['pickleball', 'padel'],
591
+ * });
769
592
  * ```
770
593
  */
771
- async submitMatch(match) {
772
- return this.submitMatches([match]);
594
+ async get(playerId, options = {}) {
595
+ const query = { id: playerId };
596
+ if (options.sport !== void 0) {
597
+ query.sport = Array.isArray(options.sport) ? options.sport.join(",") : options.sport;
598
+ }
599
+ const wire = await this.#http.request({
600
+ method: "GET",
601
+ path: "/partner/member",
602
+ query
603
+ });
604
+ return new Member(wire);
773
605
  }
774
606
  /**
775
- * Submit multiple matches in a batch.
607
+ * Search for members, yielding each match as a {@link Member}.
776
608
  *
777
- * @param matches - List of Match objects
778
- * @returns MatchResult with submission status
609
+ * This is an **auto-paginating async iterator** it fetches pages
610
+ * from the server lazily as you iterate, so you can stream through
611
+ * thousands of results without holding them all in memory:
779
612
  *
780
- * @example
781
613
  * ```ts
782
- * const result = await client.submitMatches([match1, match2, match3]);
783
- * console.log(`Submitted ${result.numGames} games from ${result.numMatches} matches`);
784
- *
785
- * if (result.dryRun) {
786
- * console.log('This was a dry run - no data persisted');
614
+ * for await (const m of client.members.search({ city: 'Austin' })) {
615
+ * console.log(m.name, m.ratingFor('pickleball'));
787
616
  * }
788
617
  * ```
618
+ *
619
+ * Stop early by `break`-ing out of the loop, or cap the total with
620
+ * `maxResults`.
789
621
  */
790
- async submitMatches(matches) {
791
- const data = await this.request("POST", "/partner/matches", {
792
- body: { matches: matches.map((m) => m.toJSON()) }
793
- });
794
- return new MatchResult(data);
622
+ async *search(filters = {}) {
623
+ const pageSize = Math.min(filters.pageSize ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
624
+ const maxResults = filters.maxResults;
625
+ const baseQuery = buildSearchQuery(filters, pageSize);
626
+ let offset = 0;
627
+ let yielded = 0;
628
+ while (true) {
629
+ const query = { ...baseQuery, offset };
630
+ const data = await this.#http.request({ method: "GET", path: "/partner/search", query });
631
+ const batch = Array.isArray(data) ? data : data?.players ?? [];
632
+ if (batch.length === 0) {
633
+ return;
634
+ }
635
+ for (const wire of batch) {
636
+ yield new Member(wire);
637
+ yielded += 1;
638
+ if (maxResults !== void 0 && yielded >= maxResults) {
639
+ return;
640
+ }
641
+ }
642
+ if (batch.length < pageSize) {
643
+ return;
644
+ }
645
+ offset += pageSize;
646
+ }
795
647
  }
796
- // ---------------------------------------------------------------------------
797
- // Rating Updates
798
- // ---------------------------------------------------------------------------
799
648
  /**
800
- * Get rating updates for subscribed members.
801
- *
802
- * Members are subscribed when you call getMember().
649
+ * Return the first search hit for a name, or `null`.
803
650
  *
804
- * @returns List of RatingUpdate objects
651
+ * Convenience for the common "look up by name" case:
805
652
  *
806
- * @example
807
653
  * ```ts
808
- * const updates = await client.getRatingUpdates();
809
- * for (const update of updates) {
810
- * console.log(`${update.memberId}: ${update.previousRating} → ${update.newRating}`);
811
- * if (update.improved) {
812
- * const member = await update.getMember();
813
- * console.log(`${member.name} improved!`);
814
- * }
654
+ * const mike = await client.members.find('Mike Barker');
655
+ * if (mike) {
656
+ * console.log(mike.ratingFor('pickleball'));
815
657
  * }
816
658
  * ```
817
659
  */
818
- async getRatingUpdates() {
819
- const data = await this.request(
820
- "GET",
821
- "/partner/rating-updates"
822
- );
823
- return (data.updates ?? []).map((u) => new RatingUpdate(u, this));
660
+ async find(name) {
661
+ for await (const member of this.search({ name, pageSize: 1, maxResults: 1 })) {
662
+ return member;
663
+ }
664
+ return null;
824
665
  }
825
666
  /**
826
- * Test webhook endpoint.
667
+ * Poll for rating change notifications.
827
668
  *
828
- * @param webhookUrl - URL to send test webhook to
829
- * @returns Test result
669
+ * Returns a list of {@link RatingUpdate} objects for every player
670
+ * whose rating has changed since the last poll. Members are
671
+ * considered subscribed when they have an active OAuth connection
672
+ * with the `webhook:subscribe` scope.
830
673
  */
831
- async testWebhook(webhookUrl) {
832
- return this.request("POST", "/partner/webhook-test", {
833
- body: { webhookUrl }
674
+ async ratingUpdates() {
675
+ const data = await this.#http.request({
676
+ method: "GET",
677
+ path: "/partner/rating-updates"
834
678
  });
679
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
680
+ return [];
681
+ }
682
+ const updates = data.updates ?? [];
683
+ return updates.map((wire) => new RatingUpdate(wire));
684
+ }
685
+ };
686
+ function buildSearchQuery(filters, pageSize) {
687
+ const query = {};
688
+ if (filters.sport !== void 0) {
689
+ query.sport = Array.isArray(filters.sport) ? filters.sport.join(",") : filters.sport;
690
+ }
691
+ if (filters.memberId !== void 0) {
692
+ query.member = String(filters.memberId);
693
+ } else if (filters.name !== void 0) {
694
+ query.member = filters.name;
695
+ }
696
+ if (filters.city !== void 0) query.city = filters.city;
697
+ if (filters.state !== void 0) query.state = filters.state;
698
+ if (filters.country !== void 0) query.country = filters.country;
699
+ if (filters.zip !== void 0) query.zip = filters.zip;
700
+ if (filters.location !== void 0) query.location = filters.location;
701
+ if (filters.gender !== void 0) {
702
+ query.gender = filters.gender.toUpperCase();
703
+ }
704
+ if (filters.vairifiedOnly !== void 0) query.vairified = filters.vairifiedOnly;
705
+ if (filters.wheelchair !== void 0) query.wheelchair = filters.wheelchair;
706
+ if (filters.ratingMin !== void 0) query.rating1 = filters.ratingMin;
707
+ if (filters.ratingMax !== void 0) query.rating2 = filters.ratingMax;
708
+ const { ageFilterType, age1, age2 } = resolveAgeFilter(filters);
709
+ if (ageFilterType !== void 0) query.ageFilterType = ageFilterType;
710
+ if (age1 !== void 0) query.age1 = age1;
711
+ if (age2 !== void 0) query.age2 = age2;
712
+ if (filters.sortBy !== void 0) query.sortField = filters.sortBy;
713
+ if (filters.sortOrder !== void 0) query.sortDirection = filters.sortOrder;
714
+ query.limit = pageSize;
715
+ return query;
716
+ }
717
+ function resolveAgeFilter(filters) {
718
+ if (filters.age !== void 0) {
719
+ return { ageFilterType: "exact", age1: filters.age };
720
+ }
721
+ if (filters.ageMin !== void 0 && filters.ageMax !== void 0) {
722
+ return { ageFilterType: "range", age1: filters.ageMin, age2: filters.ageMax };
723
+ }
724
+ if (filters.ageMin !== void 0) {
725
+ return { ageFilterType: "above", age1: filters.ageMin };
726
+ }
727
+ if (filters.ageMax !== void 0) {
728
+ return { ageFilterType: "below", age1: filters.ageMax };
729
+ }
730
+ return {};
731
+ }
732
+
733
+ // src/oauth.ts
734
+ var SCOPES = Object.freeze({
735
+ "profile:read": "Access your name, location, and verification status",
736
+ "profile:email": "Access your email address",
737
+ "rating:read": "View your current rating and rating splits",
738
+ "rating:history": "View your complete rating history",
739
+ "match:submit": "Submit match results on your behalf",
740
+ "webhook:subscribe": "Receive notifications when your rating changes"
741
+ });
742
+ var DEFAULT_SCOPES = Object.freeze(["profile:read", "rating:read"]);
743
+ function getAuthorizationUrl(config, options = {}) {
744
+ const baseUrl = (config.baseUrl ?? "https://api-next.vairified.com/api/v1").replace(/\/+$/, "");
745
+ const scopeList = ensureProfileRead(options.scopes ?? DEFAULT_SCOPES);
746
+ const params = new URLSearchParams({
747
+ redirect_uri: config.redirectUri,
748
+ scope: scopeList.join(","),
749
+ response_type: "code"
750
+ });
751
+ if (options.state) {
752
+ params.set("state", options.state);
753
+ }
754
+ return `${baseUrl}/partner/oauth/authorize?${params.toString()}`;
755
+ }
756
+ function validateScope(scope) {
757
+ return scope in SCOPES;
758
+ }
759
+ function describeScope(scope) {
760
+ if (validateScope(scope)) {
761
+ return SCOPES[scope];
762
+ }
763
+ return `Unknown scope: ${scope}`;
764
+ }
765
+ function describeScopes(scopes) {
766
+ return scopes.map((scope) => ({
767
+ scope,
768
+ description: describeScope(scope)
769
+ }));
770
+ }
771
+ function generateState() {
772
+ const bytes = new Uint8Array(32);
773
+ crypto.getRandomValues(bytes);
774
+ let binary = "";
775
+ for (const byte of bytes) {
776
+ binary += String.fromCharCode(byte);
777
+ }
778
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
779
+ }
780
+ function ensureProfileRead(scopes) {
781
+ if (scopes.includes("profile:read")) {
782
+ return scopes;
783
+ }
784
+ return ["profile:read", ...scopes];
785
+ }
786
+
787
+ // src/resources/oauth.ts
788
+ var OAuthResource = class {
789
+ #http;
790
+ /** @internal */
791
+ constructor(http) {
792
+ this.#http = http;
835
793
  }
836
- // ---------------------------------------------------------------------------
837
- // OAuth Operations
838
- // ---------------------------------------------------------------------------
839
794
  /**
840
795
  * Start an OAuth authorization flow.
841
796
  *
842
- * This creates a pending authorization and returns the URL where
843
- * users should be redirected to approve access.
844
- *
845
- * @param redirectUri - Your application's callback URL
846
- * @param scopes - Permission scopes to request (defaults to profile:read, rating:read)
847
- * @param state - CSRF protection state parameter (recommended)
848
- * @returns AuthorizationResponse with the URL to redirect users to
849
- * @throws OAuthError if the authorization fails to start
850
- *
851
- * @example
852
- * ```ts
853
- * const auth = await client.startOAuth(
854
- * 'https://myapp.com/callback',
855
- * ['profile:read', 'rating:read', 'match:submit'],
856
- * 'random_csrf_token',
857
- * );
858
- * // Redirect user to auth.authorizationUrl
859
- * window.location.href = auth.authorizationUrl;
860
- * ```
861
- *
862
- * @category OAuth
797
+ * @throws {@link OAuthError} with `errorCode: 'invalid_scope'` if a
798
+ * requested scope is not in the accepted list.
863
799
  */
864
- async startOAuth(redirectUri, scopes = [...DEFAULT_SCOPES], state) {
865
- const scopeSet = new Set(scopes);
866
- scopeSet.add("profile:read");
867
- const scopeList = Array.from(scopeSet);
800
+ async authorize(options) {
801
+ const scopeList = ensureProfileRead(options.scopes ?? DEFAULT_SCOPES);
868
802
  for (const scope of scopeList) {
869
803
  if (!(scope in SCOPES)) {
870
804
  throw new OAuthError(`Invalid scope: ${scope}`, "invalid_scope");
871
805
  }
872
806
  }
873
- const data = await this.request("POST", "/partner/oauth/authorize", {
807
+ const data = await this.#http.request({
808
+ method: "POST",
809
+ path: "/partner/oauth/authorize",
874
810
  body: {
875
- redirectUri,
811
+ redirectUri: options.redirectUri,
876
812
  scope: scopeList.join(","),
877
- state
813
+ state: options.state
878
814
  }
879
815
  });
880
816
  return {
881
- authorizationUrl: data.authorizationUrl,
882
- code: data.code,
883
- state
817
+ authorizationUrl: data?.authorizationUrl ?? "",
818
+ code: data?.code ?? "",
819
+ state: options.state
884
820
  };
885
821
  }
886
- /**
887
- * Exchange an authorization code for access and refresh tokens.
888
- *
889
- * Call this after the user approves access and is redirected back
890
- * to your application with a code parameter.
891
- *
892
- * @param code - Authorization code from the callback URL
893
- * @param redirectUri - Must match the redirectUri used in startOAuth
894
- * @returns TokenResponse with access_token, refresh_token, and player_id
895
- * @throws OAuthError if the code is invalid or expired
896
- *
897
- * @example
898
- * ```ts
899
- * // After user is redirected to: https://myapp.com/callback?code=xxx
900
- * const tokens = await client.exchangeToken(
901
- * new URL(window.location.href).searchParams.get('code')!,
902
- * 'https://myapp.com/callback',
903
- * );
904
- * // Store tokens.accessToken and tokens.refreshToken securely
905
- * // Use tokens.playerId to identify the connected player
906
- * ```
907
- *
908
- * @category OAuth
909
- */
910
- async exchangeToken(code, redirectUri) {
911
- const data = await this.request("POST", "/partner/oauth/token", {
912
- body: { code, redirectUri }
822
+ /** Exchange an authorization code for access and refresh tokens. */
823
+ async exchangeToken(options) {
824
+ const data = await this.#http.request({
825
+ method: "POST",
826
+ path: "/partner/oauth/token",
827
+ body: { code: options.code, redirectUri: options.redirectUri }
913
828
  });
914
- return {
915
- accessToken: data.accessToken,
916
- refreshToken: data.refreshToken,
917
- expiresIn: data.expiresIn,
918
- scope: data.scope ? data.scope.split(",") : [],
919
- playerId: data.playerId
920
- };
829
+ return tokenResponseFromWire(data);
921
830
  }
922
- /**
923
- * Refresh an expired access token.
924
- *
925
- * Use this when an access token expires to obtain a new one
926
- * without requiring the user to re-authorize.
927
- *
928
- * @param refreshToken - The refresh token from a previous token exchange
929
- * @returns TokenResponse with new access_token and optionally a new refresh_token
930
- * @throws OAuthError if the refresh token is invalid or revoked
931
- *
932
- * @example
933
- * ```ts
934
- * try {
935
- * const newTokens = await client.refreshAccessToken(storedRefreshToken);
936
- * // Update stored tokens
937
- * } catch (e) {
938
- * if (e instanceof OAuthError && e.errorCode === 'invalid_grant') {
939
- * // Refresh token revoked, user needs to re-authorize
940
- * }
941
- * }
942
- * ```
943
- *
944
- * @category OAuth
945
- */
946
- async refreshAccessToken(refreshToken) {
947
- const data = await this.request("POST", "/partner/oauth/refresh", {
831
+ /** Refresh an expired access token using a refresh token. */
832
+ async refresh(refreshToken) {
833
+ const data = await this.#http.request({
834
+ method: "POST",
835
+ path: "/partner/oauth/refresh",
948
836
  body: { refreshToken }
949
837
  });
950
- return {
951
- accessToken: data.accessToken,
952
- refreshToken: data.refreshToken,
953
- expiresIn: data.expiresIn,
954
- scope: data.scope ? data.scope.split(",") : [],
955
- playerId: data.playerId
956
- };
838
+ return tokenResponseFromWire(data);
839
+ }
840
+ /** Revoke a player's OAuth connection to your app. */
841
+ async revoke(playerId) {
842
+ const data = await this.#http.request({
843
+ method: "POST",
844
+ path: "/partner/oauth/revoke",
845
+ body: { playerId }
846
+ });
847
+ return data ?? {};
848
+ }
849
+ /** Return the list of OAuth scopes the server currently supports. */
850
+ async availableScopes() {
851
+ const data = await this.#http.request({
852
+ method: "GET",
853
+ path: "/partner/oauth/scopes"
854
+ });
855
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
856
+ return [];
857
+ }
858
+ return data.scopes ?? [];
859
+ }
860
+ };
861
+ function tokenResponseFromWire(data) {
862
+ const scopeRaw = data?.scope ?? "";
863
+ const scopeList = scopeRaw.length > 0 ? scopeRaw.split(",") : [];
864
+ return {
865
+ accessToken: data?.accessToken ?? "",
866
+ refreshToken: data?.refreshToken ?? null,
867
+ expiresIn: data?.expiresIn ?? 3600,
868
+ scope: Object.freeze(scopeList),
869
+ playerId: data?.playerId ?? ""
870
+ };
871
+ }
872
+
873
+ // src/client.ts
874
+ var ENVIRONMENTS = Object.freeze({
875
+ production: "https://api-next.vairified.com/api/v1",
876
+ staging: "https://api-staging.vairified.com/api/v1",
877
+ local: "http://localhost:3001/api/v1"
878
+ });
879
+ var DEFAULT_TIMEOUT_MS = 3e4;
880
+ var Vairified = class {
881
+ /** The resolved API key this client is using. */
882
+ apiKey;
883
+ /** The resolved base URL (production, staging, local, or custom). */
884
+ baseUrl;
885
+ /** The resolved environment name. */
886
+ env;
887
+ /** Request timeout in milliseconds. */
888
+ timeoutMs;
889
+ /** Member operations — get, search, find, ratingUpdates. */
890
+ members;
891
+ /** Match submission — submit, testWebhook. */
892
+ matches;
893
+ /** OAuth flow — authorize, exchangeToken, refresh, revoke. */
894
+ oauth;
895
+ /** Leaderboard queries — list, rank, categories. */
896
+ leaderboard;
897
+ #transport;
898
+ constructor(options = {}) {
899
+ const apiKey = options.apiKey ?? process.env.VAIRIFIED_API_KEY ?? "";
900
+ if (apiKey.length === 0) {
901
+ throw new Error("API key required. Pass { apiKey } or set VAIRIFIED_API_KEY.");
902
+ }
903
+ this.apiKey = apiKey;
904
+ if (options.baseUrl) {
905
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
906
+ this.env = options.env ?? "production";
907
+ } else {
908
+ const envName = options.env ?? process.env.VAIRIFIED_ENV ?? "production";
909
+ if (options.env && !(envName in ENVIRONMENTS)) {
910
+ throw new Error(
911
+ `Unknown environment: ${envName}. Use one of: ${Object.keys(ENVIRONMENTS).join(", ")}`
912
+ );
913
+ }
914
+ this.env = envName;
915
+ this.baseUrl = ENVIRONMENTS[envName] ?? ENVIRONMENTS.production;
916
+ }
917
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
918
+ this.#transport = new HttpTransport({
919
+ baseUrl: this.baseUrl,
920
+ apiKey: this.apiKey,
921
+ timeoutMs: this.timeoutMs,
922
+ fetch: options.fetch ?? fetch
923
+ });
924
+ this.members = new MembersResource(this.#transport);
925
+ this.matches = new MatchesResource(this.#transport);
926
+ this.oauth = new OAuthResource(this.#transport);
927
+ this.leaderboard = new LeaderboardResource(this.#transport);
957
928
  }
958
929
  /**
959
- * Revoke a player's OAuth connection.
960
- *
961
- * This disconnects the player from your application. You will no
962
- * longer be able to access their data or submit matches on their behalf.
963
- *
964
- * @param playerId - The player's external ID (vair_mem_xxx format)
965
- * @throws OAuthError if the revocation fails
966
- *
967
- * @example
968
- * ```ts
969
- * await client.revokeConnection('vair_mem_0ABC123def456GHI789jk');
970
- * // Player is now disconnected
971
- * ```
930
+ * API usage statistics for the current API key.
972
931
  *
973
- * @category OAuth
932
+ * Returns rate-limit status, request counts, and quota usage.
974
933
  */
975
- async revokeConnection(playerId) {
976
- await this.request("POST", "/partner/oauth/revoke", {
977
- body: { playerId }
934
+ async usage() {
935
+ const data = await this.#transport.request({
936
+ method: "GET",
937
+ path: "/partner/usage"
978
938
  });
939
+ return data ?? {};
979
940
  }
980
941
  /**
981
- * Get a list of available OAuth scopes.
982
- *
983
- * @returns List of scope objects with id, name, and description
984
- *
985
- * @example
986
- * ```ts
987
- * const scopes = await client.getAvailableScopes();
988
- * for (const scope of scopes) {
989
- * console.log(`${scope.id}: ${scope.description}`);
990
- * }
991
- * ```
942
+ * Release any resources held by the client.
992
943
  *
993
- * @category OAuth
944
+ * The current transport is stateless, so this is a no-op today, but
945
+ * partners should still call it (or use `await using`) so the SDK
946
+ * can add connection pooling later without breaking them.
994
947
  */
995
- async getAvailableScopes() {
996
- const data = await this.request("GET", "/partner/oauth/scopes");
997
- return data.scopes ?? [];
948
+ async close() {
998
949
  }
999
950
  /**
1000
- * Get API usage statistics for your partner account.
1001
- *
1002
- * @returns Usage statistics (requests, limits, etc.)
1003
- *
1004
- * @example
1005
- * ```ts
1006
- * const usage = await client.getUsage();
1007
- * console.log(`Requests today: ${usage.requestsToday}`);
1008
- * console.log(`Rate limit: ${usage.rateLimit}/hour`);
1009
- * ```
1010
- *
1011
- * @category Client
951
+ * Explicit resource management hook enables
952
+ * `await using client = new Vairified({ ... })` (TypeScript 5.2+).
1012
953
  */
1013
- async getUsage() {
1014
- return this.request("GET", "/partner/usage");
954
+ async [Symbol.asyncDispose]() {
955
+ await this.close();
956
+ }
957
+ /** Compact summary for console output. */
958
+ toString() {
959
+ return `Vairified { env: '${this.env}', baseUrl: '${this.baseUrl}' }`;
1015
960
  }
1016
961
  };
1017
962
  // Annotate the CommonJS export names for ESM import in node:
1018
963
  0 && (module.exports = {
1019
964
  AuthenticationError,
1020
965
  DEFAULT_SCOPES,
1021
- Match,
1022
- MatchResult,
966
+ ENVIRONMENTS,
967
+ LeaderboardResource,
968
+ MatchBatchResult,
969
+ MatchesResource,
1023
970
  Member,
971
+ MemberSportMap,
972
+ MembersResource,
1024
973
  NotFoundError,
1025
974
  OAuthError,
1026
- Player,
975
+ OAuthResource,
1027
976
  RateLimitError,
1028
- RatingSplit,
1029
- RatingSplits,
1030
977
  RatingUpdate,
1031
978
  SCOPES,
1032
- SearchResults,
979
+ SportRating,
1033
980
  Vairified,
1034
981
  VairifiedError,
1035
982
  ValidationError,