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