vairified 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -250
- package/dist/index.cjs +950 -802
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1016 -767
- package/dist/index.d.ts +1016 -767
- package/dist/index.js +938 -796
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -22,21 +22,27 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AuthenticationError: () => AuthenticationError,
|
|
24
24
|
DEFAULT_SCOPES: () => DEFAULT_SCOPES,
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
34
|
+
OAuthResource: () => OAuthResource,
|
|
31
35
|
RateLimitError: () => RateLimitError,
|
|
32
|
-
RatingSplit: () => RatingSplit,
|
|
33
|
-
RatingSplits: () => RatingSplits,
|
|
34
36
|
RatingUpdate: () => RatingUpdate,
|
|
35
37
|
SCOPES: () => SCOPES,
|
|
36
|
-
|
|
38
|
+
SportRating: () => SportRating,
|
|
39
|
+
TournamentImportResult: () => TournamentImportResult,
|
|
37
40
|
Vairified: () => Vairified,
|
|
38
41
|
VairifiedError: () => VairifiedError,
|
|
39
42
|
ValidationError: () => ValidationError,
|
|
43
|
+
WebhookDeliveriesResult: () => WebhookDeliveriesResult,
|
|
44
|
+
WebhookDelivery: () => WebhookDelivery,
|
|
45
|
+
WebhooksResource: () => WebhooksResource,
|
|
40
46
|
describeScope: () => describeScope,
|
|
41
47
|
describeScopes: () => describeScopes,
|
|
42
48
|
generateState: () => generateState,
|
|
@@ -47,9 +53,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
47
53
|
|
|
48
54
|
// src/errors.ts
|
|
49
55
|
var VairifiedError = class extends Error {
|
|
50
|
-
/** HTTP status code */
|
|
56
|
+
/** HTTP status code (if the error came from an API response). */
|
|
51
57
|
statusCode;
|
|
52
|
-
/**
|
|
58
|
+
/** Raw response body parsed as JSON when available. */
|
|
53
59
|
response;
|
|
54
60
|
constructor(message, statusCode, response) {
|
|
55
61
|
super(message);
|
|
@@ -59,7 +65,7 @@ var VairifiedError = class extends Error {
|
|
|
59
65
|
}
|
|
60
66
|
};
|
|
61
67
|
var RateLimitError = class extends VairifiedError {
|
|
62
|
-
/** Seconds to wait before retrying */
|
|
68
|
+
/** Seconds to wait before retrying, or `undefined` if the server didn't say. */
|
|
63
69
|
retryAfter;
|
|
64
70
|
constructor(message = "Rate limit exceeded", retryAfter, response) {
|
|
65
71
|
super(message, 429, response);
|
|
@@ -86,7 +92,10 @@ var ValidationError = class extends VairifiedError {
|
|
|
86
92
|
}
|
|
87
93
|
};
|
|
88
94
|
var OAuthError = class extends VairifiedError {
|
|
89
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* OAuth error code such as `'invalid_grant'`, `'invalid_scope'`,
|
|
97
|
+
* or `'expired_token'`. Check this to branch on the specific failure.
|
|
98
|
+
*/
|
|
90
99
|
errorCode;
|
|
91
100
|
constructor(message = "OAuth error", errorCode, response) {
|
|
92
101
|
super(message, void 0, response);
|
|
@@ -95,944 +104,1083 @@ var OAuthError = class extends VairifiedError {
|
|
|
95
104
|
}
|
|
96
105
|
};
|
|
97
106
|
|
|
98
|
-
// src/
|
|
99
|
-
var
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
// src/http.ts
|
|
108
|
+
var HttpTransport = class {
|
|
109
|
+
#config;
|
|
110
|
+
constructor(config) {
|
|
111
|
+
this.#config = config;
|
|
112
|
+
}
|
|
113
|
+
async request(options) {
|
|
114
|
+
const url = buildUrl(this.#config.baseUrl, options.path, options.query);
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
const timeoutId = setTimeout(
|
|
117
|
+
() => controller.abort(new Error(`Request timed out after ${this.#config.timeoutMs}ms`)),
|
|
118
|
+
this.#config.timeoutMs
|
|
119
|
+
);
|
|
120
|
+
const headers = {
|
|
121
|
+
"X-API-Key": this.#config.apiKey,
|
|
122
|
+
Accept: "application/json"
|
|
123
|
+
};
|
|
124
|
+
const init = {
|
|
125
|
+
method: options.method,
|
|
126
|
+
headers,
|
|
127
|
+
signal: controller.signal
|
|
128
|
+
};
|
|
129
|
+
if (options.body !== void 0) {
|
|
130
|
+
headers["Content-Type"] = "application/json";
|
|
131
|
+
init.body = JSON.stringify(options.body);
|
|
115
132
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
}
|
|
133
|
+
let response;
|
|
134
|
+
try {
|
|
135
|
+
response = await this.#config.fetch(url, init);
|
|
136
|
+
} finally {
|
|
137
|
+
clearTimeout(timeoutId);
|
|
127
138
|
}
|
|
128
|
-
|
|
129
|
-
|
|
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 };
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
await throwFromResponse(response);
|
|
163
141
|
}
|
|
164
|
-
|
|
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);
|
|
142
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
143
|
+
return void 0;
|
|
210
144
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return `${this.firstName} ${this.lastName}`.trim();
|
|
145
|
+
const text = await response.text();
|
|
146
|
+
if (text.length === 0) {
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(text);
|
|
151
|
+
} catch {
|
|
152
|
+
throw new VairifiedError(`Unable to parse response as JSON: ${text}`, response.status);
|
|
220
153
|
}
|
|
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
154
|
}
|
|
231
155
|
};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
/** Refresh member data from API */
|
|
247
|
-
async refresh() {
|
|
248
|
-
if (!this._client) {
|
|
249
|
-
throw new Error("Member not connected to client");
|
|
156
|
+
function buildUrl(baseUrl, path, query) {
|
|
157
|
+
const cleanBase = baseUrl.replace(/\/+$/, "");
|
|
158
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
159
|
+
const url = new URL(cleanBase + cleanPath);
|
|
160
|
+
if (query) {
|
|
161
|
+
for (const [key, value] of Object.entries(query)) {
|
|
162
|
+
if (value === null || value === void 0) continue;
|
|
163
|
+
if (Array.isArray(value)) {
|
|
164
|
+
if (value.length === 0) continue;
|
|
165
|
+
url.searchParams.set(key, value.join(","));
|
|
166
|
+
} else {
|
|
167
|
+
url.searchParams.set(key, String(value));
|
|
168
|
+
}
|
|
250
169
|
}
|
|
251
|
-
const updated = await this._client.getMember(this.id);
|
|
252
|
-
Object.assign(this, updated);
|
|
253
|
-
return this;
|
|
254
170
|
}
|
|
255
|
-
|
|
256
|
-
function generateId() {
|
|
257
|
-
return `SDK-${Math.random().toString(36).substring(2, 14)}`;
|
|
171
|
+
return url.toString();
|
|
258
172
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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");
|
|
173
|
+
async function throwFromResponse(response) {
|
|
174
|
+
const status = response.status;
|
|
175
|
+
const text = await response.text().catch(() => "");
|
|
176
|
+
let body = null;
|
|
177
|
+
if (text.length > 0) {
|
|
178
|
+
try {
|
|
179
|
+
body = JSON.parse(text);
|
|
180
|
+
} catch {
|
|
181
|
+
body = null;
|
|
320
182
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
183
|
+
}
|
|
184
|
+
let message;
|
|
185
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
186
|
+
const apiBody = body;
|
|
187
|
+
message = apiBody.message || apiBody.error || text || `HTTP ${status}`;
|
|
188
|
+
} else {
|
|
189
|
+
message = text || `HTTP ${status}`;
|
|
190
|
+
}
|
|
191
|
+
switch (status) {
|
|
192
|
+
case 400:
|
|
193
|
+
throw new ValidationError(message, body);
|
|
194
|
+
case 401:
|
|
195
|
+
throw new AuthenticationError(message, body);
|
|
196
|
+
case 404:
|
|
197
|
+
throw new NotFoundError(message, body);
|
|
198
|
+
case 429: {
|
|
199
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
200
|
+
const retryAfter = retryAfterHeader ? Number.parseInt(retryAfterHeader, 10) : void 0;
|
|
201
|
+
throw new RateLimitError(message, Number.isFinite(retryAfter) ? retryAfter : void 0, body);
|
|
333
202
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
203
|
+
default:
|
|
204
|
+
throw new VairifiedError(message, status, body);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/resources/leaderboard.ts
|
|
209
|
+
var LeaderboardResource = class {
|
|
210
|
+
#http;
|
|
211
|
+
/** @internal */
|
|
212
|
+
constructor(http) {
|
|
213
|
+
this.#http = http;
|
|
214
|
+
}
|
|
215
|
+
/** Fetch a leaderboard page with optional filters. */
|
|
216
|
+
async list(options = {}) {
|
|
217
|
+
const query = {
|
|
218
|
+
limit: options.limit ?? 50,
|
|
219
|
+
offset: options.offset ?? 0,
|
|
220
|
+
category: options.category,
|
|
221
|
+
ageBracket: options.ageBracket,
|
|
222
|
+
scope: options.scope,
|
|
223
|
+
state: options.state,
|
|
224
|
+
city: options.city,
|
|
225
|
+
clubId: options.clubId,
|
|
226
|
+
gender: options.gender?.toUpperCase(),
|
|
227
|
+
minGames: options.minGames,
|
|
228
|
+
search: options.search,
|
|
229
|
+
verifiedOnly: options.verifiedOnly === true ? true : void 0
|
|
345
230
|
};
|
|
231
|
+
const data = await this.#http.request({
|
|
232
|
+
method: "GET",
|
|
233
|
+
path: "/leaderboard",
|
|
234
|
+
query
|
|
235
|
+
});
|
|
236
|
+
return data ?? {};
|
|
237
|
+
}
|
|
238
|
+
/** Fetch a specific player's rank plus nearby players. */
|
|
239
|
+
async rank(playerId, options = {}) {
|
|
240
|
+
const body = {
|
|
241
|
+
playerId,
|
|
242
|
+
category: options.category ?? "doubles",
|
|
243
|
+
ageBracket: options.ageBracket ?? "open",
|
|
244
|
+
scope: options.scope ?? "global",
|
|
245
|
+
contextSize: options.contextSize ?? 5
|
|
246
|
+
};
|
|
247
|
+
if (options.state !== void 0) body.state = options.state;
|
|
248
|
+
if (options.city !== void 0) body.city = options.city;
|
|
249
|
+
if (options.clubId !== void 0) body.clubId = options.clubId;
|
|
250
|
+
const data = await this.#http.request({
|
|
251
|
+
method: "POST",
|
|
252
|
+
path: "/leaderboard/rank",
|
|
253
|
+
body
|
|
254
|
+
});
|
|
255
|
+
return data ?? {};
|
|
256
|
+
}
|
|
257
|
+
/** List available leaderboard categories, brackets, and scopes. */
|
|
258
|
+
async categories() {
|
|
259
|
+
const data = await this.#http.request({
|
|
260
|
+
method: "GET",
|
|
261
|
+
path: "/leaderboard/categories"
|
|
262
|
+
});
|
|
263
|
+
return data ?? {};
|
|
346
264
|
}
|
|
347
265
|
};
|
|
348
|
-
|
|
349
|
-
|
|
266
|
+
|
|
267
|
+
// src/models/match-batch-result.ts
|
|
268
|
+
var MatchBatchResult = class {
|
|
350
269
|
success;
|
|
351
|
-
/** Number of matches processed */
|
|
352
270
|
numMatches;
|
|
353
|
-
/** Number of games recorded */
|
|
354
271
|
numGames;
|
|
355
|
-
/** Whether this was a dry-run (validation only) */
|
|
356
272
|
dryRun;
|
|
357
|
-
/** Human-readable result message */
|
|
358
273
|
message;
|
|
359
|
-
/** List of validation/processing errors */
|
|
360
274
|
errors;
|
|
361
|
-
constructor(
|
|
362
|
-
this.success =
|
|
363
|
-
this.numMatches =
|
|
364
|
-
this.numGames =
|
|
365
|
-
this.dryRun =
|
|
366
|
-
this.message =
|
|
367
|
-
this.errors =
|
|
368
|
-
|
|
369
|
-
|
|
275
|
+
constructor(wire) {
|
|
276
|
+
this.success = wire.success;
|
|
277
|
+
this.numMatches = wire.numMatches;
|
|
278
|
+
this.numGames = wire.numGames;
|
|
279
|
+
this.dryRun = wire.dryRun ?? null;
|
|
280
|
+
this.message = wire.message ?? null;
|
|
281
|
+
this.errors = wire.errors ? Object.freeze([...wire.errors]) : null;
|
|
282
|
+
Object.freeze(this);
|
|
283
|
+
}
|
|
284
|
+
/** Shorthand: successful submission with zero errors. */
|
|
285
|
+
get ok() {
|
|
286
|
+
return this.success && (this.errors === null || this.errors.length === 0);
|
|
287
|
+
}
|
|
288
|
+
/** Whether this was a dry-run (validation only, nothing persisted). */
|
|
370
289
|
get isDryRun() {
|
|
371
|
-
return this.dryRun;
|
|
290
|
+
return this.dryRun === true;
|
|
291
|
+
}
|
|
292
|
+
toString() {
|
|
293
|
+
const mode = this.isDryRun ? " [dry-run]" : "";
|
|
294
|
+
const errs = this.errors && this.errors.length > 0 ? ` errors=${this.errors.length}` : "";
|
|
295
|
+
const status = this.ok ? "ok" : "FAILED";
|
|
296
|
+
return `MatchBatchResult ${status}${mode} matches=${this.numMatches} games=${this.numGames}${errs}`;
|
|
372
297
|
}
|
|
373
|
-
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/models/tournament-import-result.ts
|
|
301
|
+
var TournamentImportResult = class {
|
|
302
|
+
success;
|
|
303
|
+
matchesImported;
|
|
304
|
+
gamesRecorded;
|
|
305
|
+
ghostPlayersCreated;
|
|
306
|
+
existingPlayersMatched;
|
|
307
|
+
dryRun;
|
|
308
|
+
message;
|
|
309
|
+
errors;
|
|
310
|
+
/** @internal */
|
|
311
|
+
constructor(wire) {
|
|
312
|
+
this.success = wire.success;
|
|
313
|
+
this.matchesImported = wire.matchesImported;
|
|
314
|
+
this.gamesRecorded = wire.gamesRecorded;
|
|
315
|
+
this.ghostPlayersCreated = wire.ghostPlayersCreated;
|
|
316
|
+
this.existingPlayersMatched = wire.existingPlayersMatched;
|
|
317
|
+
this.dryRun = wire.dryRun ?? false;
|
|
318
|
+
this.message = wire.message;
|
|
319
|
+
this.errors = Object.freeze(wire.errors ?? []);
|
|
320
|
+
Object.freeze(this);
|
|
321
|
+
}
|
|
322
|
+
/** True when the import succeeded without errors. */
|
|
374
323
|
get ok() {
|
|
375
324
|
return this.success && this.errors.length === 0;
|
|
376
325
|
}
|
|
377
326
|
};
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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;
|
|
327
|
+
|
|
328
|
+
// src/resources/matches.ts
|
|
329
|
+
var MatchesResource = class {
|
|
330
|
+
#http;
|
|
331
|
+
/** @internal */
|
|
332
|
+
constructor(http) {
|
|
333
|
+
this.#http = http;
|
|
404
334
|
}
|
|
405
|
-
/**
|
|
406
|
-
|
|
407
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Submit a {@link MatchBatch} for rating calculation.
|
|
337
|
+
*
|
|
338
|
+
* All players in every match must have granted the `user:match:submit`
|
|
339
|
+
* scope via OAuth (unless your API key has the
|
|
340
|
+
* `user:match:submit:trusted` scope, which skips per-player consent).
|
|
341
|
+
*
|
|
342
|
+
* Set `batch.dryRun = true` to validate without persisting.
|
|
343
|
+
*
|
|
344
|
+
* ```ts
|
|
345
|
+
* const result = await client.matches.submit({
|
|
346
|
+
* sport: 'pickleball',
|
|
347
|
+
* winScore: 11,
|
|
348
|
+
* winBy: 2,
|
|
349
|
+
* bracket: '4.0 Doubles',
|
|
350
|
+
* event: 'Weekly League',
|
|
351
|
+
* matchDate: '2026-04-11T14:00:00Z',
|
|
352
|
+
* matches: [
|
|
353
|
+
* {
|
|
354
|
+
* identifier: 'm1',
|
|
355
|
+
* teams: [['vair_mem_aaa', 'vair_mem_bbb'],
|
|
356
|
+
* ['vair_mem_ccc', 'vair_mem_ddd']],
|
|
357
|
+
* games: [{ scores: [11, 8] }, { scores: [11, 5] }],
|
|
358
|
+
* },
|
|
359
|
+
* ],
|
|
360
|
+
* });
|
|
361
|
+
* if (result.ok) {
|
|
362
|
+
* console.log(`Submitted ${result.numGames} games`);
|
|
363
|
+
* }
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
async submit(batch) {
|
|
367
|
+
const wire = await this.#http.request({
|
|
368
|
+
method: "POST",
|
|
369
|
+
path: "/partner/matches",
|
|
370
|
+
body: batch
|
|
371
|
+
});
|
|
372
|
+
return new MatchBatchResult(wire);
|
|
408
373
|
}
|
|
409
|
-
/**
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Import tournament results.
|
|
376
|
+
*
|
|
377
|
+
* The request body is a free-form JSON object whose structure is
|
|
378
|
+
* defined by the Vairified tournament import schema. Set
|
|
379
|
+
* `body.dryRun = true` to validate without persisting.
|
|
380
|
+
*
|
|
381
|
+
* @param body - Tournament import payload.
|
|
382
|
+
* @returns {@link TournamentImportResult} with match/game counts.
|
|
383
|
+
* @category Matches
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* ```ts
|
|
387
|
+
* const result = await client.matches.tournamentImport({
|
|
388
|
+
* sport: 'pickleball',
|
|
389
|
+
* tournamentName: 'Spring Classic',
|
|
390
|
+
* matches: [...],
|
|
391
|
+
* });
|
|
392
|
+
* if (result.ok) {
|
|
393
|
+
* console.log(`Imported ${result.matchesImported} matches`);
|
|
394
|
+
* }
|
|
395
|
+
* ```
|
|
396
|
+
*/
|
|
397
|
+
async tournamentImport(body) {
|
|
398
|
+
const wire = await this.#http.request({
|
|
399
|
+
method: "POST",
|
|
400
|
+
path: "/partner/tournament-import",
|
|
401
|
+
body
|
|
402
|
+
});
|
|
403
|
+
return new TournamentImportResult(wire);
|
|
415
404
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
|
|
405
|
+
/** Send a test payload to a webhook URL. */
|
|
406
|
+
async testWebhook(webhookUrl) {
|
|
407
|
+
const data = await this.#http.request({
|
|
408
|
+
method: "POST",
|
|
409
|
+
path: "/partner/webhook-test",
|
|
410
|
+
body: { webhookUrl }
|
|
411
|
+
});
|
|
412
|
+
return data ?? {};
|
|
420
413
|
}
|
|
421
414
|
};
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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]();
|
|
415
|
+
|
|
416
|
+
// src/models/sport-rating.ts
|
|
417
|
+
var SportRating = class {
|
|
418
|
+
/** Primary rating for this sport. */
|
|
419
|
+
rating;
|
|
420
|
+
/** Category abbreviation for the primary rating (e.g. `'VO'`). */
|
|
421
|
+
abbr;
|
|
422
|
+
#splits;
|
|
423
|
+
constructor(wire) {
|
|
424
|
+
this.rating = wire.rating;
|
|
425
|
+
this.abbr = wire.abbr;
|
|
426
|
+
this.#splits = new Map(Object.entries(wire.ratingSplits ?? {}));
|
|
427
|
+
Object.freeze(this);
|
|
460
428
|
}
|
|
461
|
-
/**
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
429
|
+
/**
|
|
430
|
+
* Look up a rating split by key (e.g. `'overall-open'`,
|
|
431
|
+
* `'singles-12-13'`, `'gender-40+'`). Returns `undefined` if the
|
|
432
|
+
* player has no rating for that bracket.
|
|
433
|
+
*/
|
|
434
|
+
get(key) {
|
|
435
|
+
return this.#splits.get(key);
|
|
436
|
+
}
|
|
437
|
+
/** Whether the player has a rating for the given split key. */
|
|
438
|
+
has(key) {
|
|
439
|
+
return this.#splits.has(key);
|
|
440
|
+
}
|
|
441
|
+
/** Number of rating splits. */
|
|
442
|
+
get size() {
|
|
443
|
+
return this.#splits.size;
|
|
444
|
+
}
|
|
445
|
+
/** All split keys the player has ratings for. */
|
|
446
|
+
keys() {
|
|
447
|
+
return this.#splits.keys();
|
|
448
|
+
}
|
|
449
|
+
/** All rating splits the player has. */
|
|
450
|
+
values() {
|
|
451
|
+
return this.#splits.values();
|
|
452
|
+
}
|
|
453
|
+
/** `[key, split]` pairs for every rating split. */
|
|
454
|
+
entries() {
|
|
455
|
+
return this.#splits.entries();
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* `for (const [key, split] of sportRating) { ... }` — iterate every
|
|
459
|
+
* rating split the player has in this sport.
|
|
460
|
+
*/
|
|
461
|
+
[Symbol.iterator]() {
|
|
462
|
+
return this.#splits.entries();
|
|
473
463
|
}
|
|
474
464
|
};
|
|
475
465
|
|
|
476
|
-
// src/
|
|
477
|
-
var
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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);
|
|
466
|
+
// src/models/member.ts
|
|
467
|
+
var MemberSportMap = class {
|
|
468
|
+
#sports;
|
|
469
|
+
constructor(wire) {
|
|
470
|
+
const entries = Object.entries(wire ?? {}).map(
|
|
471
|
+
([code, w]) => [code, new SportRating(w)]
|
|
472
|
+
);
|
|
473
|
+
this.#sports = new Map(entries);
|
|
474
|
+
Object.freeze(this);
|
|
498
475
|
}
|
|
499
|
-
|
|
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
|
-
}
|
|
476
|
+
get(sport) {
|
|
477
|
+
return this.#sports.get(sport);
|
|
521
478
|
}
|
|
522
|
-
|
|
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
|
-
};
|
|
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;
|
|
479
|
+
has(sport) {
|
|
480
|
+
return this.#sports.has(sport);
|
|
560
481
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
return process.env[name];
|
|
564
|
-
}
|
|
565
|
-
return "";
|
|
482
|
+
get size() {
|
|
483
|
+
return this.#sports.size;
|
|
566
484
|
}
|
|
567
|
-
|
|
568
|
-
return this.
|
|
485
|
+
keys() {
|
|
486
|
+
return this.#sports.keys();
|
|
569
487
|
}
|
|
570
|
-
|
|
571
|
-
return
|
|
572
|
-
"X-API-Key": this.apiKey,
|
|
573
|
-
"Content-Type": "application/json",
|
|
574
|
-
Accept: "application/json"
|
|
575
|
-
};
|
|
488
|
+
values() {
|
|
489
|
+
return this.#sports.values();
|
|
576
490
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
491
|
+
entries() {
|
|
492
|
+
return this.#sports.entries();
|
|
493
|
+
}
|
|
494
|
+
[Symbol.iterator]() {
|
|
495
|
+
return this.#sports.entries();
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
var Member = class {
|
|
499
|
+
memberId;
|
|
500
|
+
id;
|
|
501
|
+
firstName;
|
|
502
|
+
lastName;
|
|
503
|
+
fullName;
|
|
504
|
+
displayName;
|
|
505
|
+
age;
|
|
506
|
+
city;
|
|
507
|
+
state;
|
|
508
|
+
zip;
|
|
509
|
+
country;
|
|
510
|
+
gender;
|
|
511
|
+
status;
|
|
512
|
+
sport;
|
|
513
|
+
activeLeagues;
|
|
514
|
+
email;
|
|
515
|
+
grantedScopes;
|
|
516
|
+
constructor(wire) {
|
|
517
|
+
this.memberId = wire.memberId;
|
|
518
|
+
this.id = wire.id ?? null;
|
|
519
|
+
this.firstName = wire.firstName;
|
|
520
|
+
this.lastName = wire.lastName;
|
|
521
|
+
this.fullName = wire.fullName;
|
|
522
|
+
this.displayName = wire.displayName;
|
|
523
|
+
this.age = wire.age ?? null;
|
|
524
|
+
this.city = wire.city ?? null;
|
|
525
|
+
this.state = wire.state ?? null;
|
|
526
|
+
this.zip = wire.zip ?? null;
|
|
527
|
+
this.country = wire.country ?? null;
|
|
528
|
+
this.gender = wire.gender ?? null;
|
|
529
|
+
this.status = Object.freeze({ ...wire.status });
|
|
530
|
+
this.sport = new MemberSportMap(wire.sport);
|
|
531
|
+
this.activeLeagues = wire.activeLeagues ? Object.freeze([...wire.activeLeagues]) : null;
|
|
532
|
+
this.email = wire.email ?? null;
|
|
533
|
+
this.grantedScopes = wire.grantedScopes ? Object.freeze([...wire.grantedScopes]) : null;
|
|
534
|
+
Object.freeze(this);
|
|
535
|
+
}
|
|
536
|
+
/** Full name — alias for {@link fullName}, matching common usage. */
|
|
537
|
+
get name() {
|
|
538
|
+
return this.fullName;
|
|
539
|
+
}
|
|
540
|
+
/** The list of sport codes this player has ratings in. */
|
|
541
|
+
get sports() {
|
|
542
|
+
return [...this.sport.keys()];
|
|
626
543
|
}
|
|
627
|
-
// ---------------------------------------------------------------------------
|
|
628
|
-
// Member Operations
|
|
629
|
-
// ---------------------------------------------------------------------------
|
|
630
544
|
/**
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
* **Requires OAuth Connection**: The player must have connected their
|
|
634
|
-
* account to your application via OAuth before you can access their data.
|
|
545
|
+
* Primary rating for a given sport.
|
|
635
546
|
*
|
|
636
|
-
* @param
|
|
637
|
-
* @returns
|
|
638
|
-
*
|
|
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
|
-
* ```
|
|
547
|
+
* @param sport Sport code — defaults to `'pickleball'`.
|
|
548
|
+
* @returns The primary rating value, or `null` if the player has no
|
|
549
|
+
* ratings for that sport.
|
|
648
550
|
*/
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
params: { id: playerId }
|
|
652
|
-
});
|
|
653
|
-
return new Member(data, this);
|
|
551
|
+
ratingFor(sport = "pickleball") {
|
|
552
|
+
return this.sport.get(sport)?.rating ?? null;
|
|
654
553
|
}
|
|
655
|
-
// ---------------------------------------------------------------------------
|
|
656
|
-
// Search Operations
|
|
657
|
-
// ---------------------------------------------------------------------------
|
|
658
554
|
/**
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
* @param filters - Search filters
|
|
662
|
-
* @returns SearchResults with players and pagination
|
|
555
|
+
* Get a specific rating split for a sport.
|
|
663
556
|
*
|
|
664
|
-
* @
|
|
665
|
-
*
|
|
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
|
-
* ```
|
|
557
|
+
* @param key Split key (e.g. `'overall-open'`).
|
|
558
|
+
* @param sport Sport code — defaults to `'pickleball'`.
|
|
681
559
|
*/
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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;
|
|
560
|
+
split(key, sport = "pickleball") {
|
|
561
|
+
return this.sport.get(sport)?.get(key) ?? null;
|
|
562
|
+
}
|
|
563
|
+
/** Compact summary for console output. */
|
|
564
|
+
toString() {
|
|
565
|
+
const primary = this.sport.values().next().value;
|
|
566
|
+
if (primary) {
|
|
567
|
+
return `Member #${this.memberId} '${this.displayName}' rating=${primary.rating.toFixed(
|
|
568
|
+
3
|
|
569
|
+
)} ${primary.abbr}`;
|
|
712
570
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
571
|
+
return `Member #${this.memberId} '${this.displayName}'`;
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// src/models/rating-update.ts
|
|
576
|
+
var RatingUpdate = class {
|
|
577
|
+
memberId;
|
|
578
|
+
id;
|
|
579
|
+
displayName;
|
|
580
|
+
sport;
|
|
581
|
+
previousRating;
|
|
582
|
+
newRating;
|
|
583
|
+
changedAt;
|
|
584
|
+
ratingSplits;
|
|
585
|
+
constructor(wire) {
|
|
586
|
+
this.memberId = wire.memberId;
|
|
587
|
+
this.id = wire.id ?? null;
|
|
588
|
+
this.displayName = wire.displayName ?? null;
|
|
589
|
+
this.sport = wire.sport ?? null;
|
|
590
|
+
this.previousRating = wire.previousRating ?? null;
|
|
591
|
+
this.newRating = wire.newRating ?? null;
|
|
592
|
+
this.changedAt = wire.changedAt ?? null;
|
|
593
|
+
this.ratingSplits = wire.ratingSplits ? Object.freeze({ ...wire.ratingSplits }) : null;
|
|
594
|
+
Object.freeze(this);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Rating change amount — `newRating - previousRating`. Returns `null`
|
|
598
|
+
* if either rating is missing from the update payload.
|
|
599
|
+
*/
|
|
600
|
+
get delta() {
|
|
601
|
+
if (this.previousRating === null || this.newRating === null) {
|
|
602
|
+
return null;
|
|
716
603
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
604
|
+
return this.newRating - this.previousRating;
|
|
605
|
+
}
|
|
606
|
+
/** `true` when the new rating is strictly higher than the previous. */
|
|
607
|
+
get improved() {
|
|
608
|
+
const delta = this.delta;
|
|
609
|
+
return delta !== null && delta > 0;
|
|
610
|
+
}
|
|
611
|
+
toString() {
|
|
612
|
+
const arrow = this.improved ? "\u2191" : "\u2193";
|
|
613
|
+
const prev = this.previousRating !== null ? this.previousRating.toFixed(3) : "?";
|
|
614
|
+
const next = this.newRating !== null ? this.newRating.toFixed(3) : "?";
|
|
615
|
+
const name = this.displayName ? ` '${this.displayName}'` : "";
|
|
616
|
+
return `RatingUpdate #${this.memberId}${name} ${prev} ${arrow} ${next}`;
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
// src/resources/members.ts
|
|
621
|
+
var DEFAULT_PAGE_SIZE = 20;
|
|
622
|
+
var MAX_PAGE_SIZE = 100;
|
|
623
|
+
var MembersResource = class {
|
|
624
|
+
#http;
|
|
625
|
+
/** @internal */
|
|
626
|
+
constructor(http) {
|
|
627
|
+
this.#http = http;
|
|
726
628
|
}
|
|
727
629
|
/**
|
|
728
|
-
*
|
|
630
|
+
* Get a connected member by external ID.
|
|
729
631
|
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
632
|
+
* **Requires an active OAuth connection** between your partner app
|
|
633
|
+
* and the player. Use the OAuth flow on `client.oauth` first.
|
|
634
|
+
*
|
|
635
|
+
* @param playerId External player ID in `vair_mem_xxx` format.
|
|
636
|
+
* @param options.sport Optional sport filter — single code or list.
|
|
637
|
+
* When omitted, the response contains every sport the player has
|
|
638
|
+
* ratings in.
|
|
639
|
+
* @throws {@link NotFoundError} if the external ID is unknown.
|
|
640
|
+
* @throws {@link VairifiedError} if the player has not connected to
|
|
641
|
+
* your app (403) or the request otherwise fails.
|
|
732
642
|
*
|
|
733
643
|
* @example
|
|
734
644
|
* ```ts
|
|
735
|
-
* const
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
645
|
+
* const member = await client.members.get('vair_mem_xxx');
|
|
646
|
+
* console.log(member.name, member.ratingFor('pickleball'));
|
|
647
|
+
*
|
|
648
|
+
* // Just pickleball
|
|
649
|
+
* const member2 = await client.members.get('vair_mem_xxx', { sport: 'pickleball' });
|
|
650
|
+
*
|
|
651
|
+
* // Multiple sports
|
|
652
|
+
* const member3 = await client.members.get('vair_mem_xxx', {
|
|
653
|
+
* sport: ['pickleball', 'padel'],
|
|
654
|
+
* });
|
|
739
655
|
* ```
|
|
740
656
|
*/
|
|
741
|
-
async
|
|
742
|
-
const
|
|
743
|
-
|
|
657
|
+
async get(playerId, options = {}) {
|
|
658
|
+
const query = { id: playerId };
|
|
659
|
+
if (options.sport !== void 0) {
|
|
660
|
+
query.sport = Array.isArray(options.sport) ? options.sport.join(",") : options.sport;
|
|
661
|
+
}
|
|
662
|
+
const wire = await this.#http.request({
|
|
663
|
+
method: "GET",
|
|
664
|
+
path: "/partner/member",
|
|
665
|
+
query
|
|
666
|
+
});
|
|
667
|
+
return new Member(wire);
|
|
744
668
|
}
|
|
745
|
-
// ---------------------------------------------------------------------------
|
|
746
|
-
// Match Operations
|
|
747
|
-
// ---------------------------------------------------------------------------
|
|
748
669
|
/**
|
|
749
|
-
*
|
|
670
|
+
* Search for members, yielding each match as a {@link Member}.
|
|
750
671
|
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
672
|
+
* This is an **auto-paginating async iterator** — it fetches pages
|
|
673
|
+
* from the server lazily as you iterate, so you can stream through
|
|
674
|
+
* thousands of results without holding them all in memory:
|
|
753
675
|
*
|
|
754
|
-
* @example
|
|
755
676
|
* ```ts
|
|
756
|
-
* const
|
|
757
|
-
*
|
|
758
|
-
* bracket: '4.0 Doubles',
|
|
759
|
-
* date: new Date(),
|
|
760
|
-
* team1: ['p1', 'p2'],
|
|
761
|
-
* team2: ['p3', 'p4'],
|
|
762
|
-
* scores: [[11, 9], [11, 7]],
|
|
763
|
-
* });
|
|
764
|
-
*
|
|
765
|
-
* const result = await client.submitMatch(match);
|
|
766
|
-
* if (result.ok) {
|
|
767
|
-
* console.log(`Submitted ${result.numGames} games`);
|
|
677
|
+
* for await (const m of client.members.search({ city: 'Austin' })) {
|
|
678
|
+
* console.log(m.name, m.ratingFor('pickleball'));
|
|
768
679
|
* }
|
|
769
680
|
* ```
|
|
681
|
+
*
|
|
682
|
+
* Stop early by `break`-ing out of the loop, or cap the total with
|
|
683
|
+
* `maxResults`.
|
|
770
684
|
*/
|
|
771
|
-
async
|
|
772
|
-
|
|
685
|
+
async *search(filters = {}) {
|
|
686
|
+
const pageSize = Math.min(filters.pageSize ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
|
|
687
|
+
const maxResults = filters.maxResults;
|
|
688
|
+
const baseQuery = buildSearchQuery(filters, pageSize);
|
|
689
|
+
let offset = 0;
|
|
690
|
+
let yielded = 0;
|
|
691
|
+
while (true) {
|
|
692
|
+
const query = { ...baseQuery, offset };
|
|
693
|
+
const data = await this.#http.request({ method: "GET", path: "/partner/search", query });
|
|
694
|
+
const batch = Array.isArray(data) ? data : data?.players ?? [];
|
|
695
|
+
if (batch.length === 0) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
for (const wire of batch) {
|
|
699
|
+
yield new Member(wire);
|
|
700
|
+
yielded += 1;
|
|
701
|
+
if (maxResults !== void 0 && yielded >= maxResults) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (batch.length < pageSize) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
offset += pageSize;
|
|
709
|
+
}
|
|
773
710
|
}
|
|
774
711
|
/**
|
|
775
|
-
*
|
|
712
|
+
* Return the first search hit for a name, or `null`.
|
|
776
713
|
*
|
|
777
|
-
*
|
|
778
|
-
* @returns MatchResult with submission status
|
|
714
|
+
* Convenience for the common "look up by name" case:
|
|
779
715
|
*
|
|
780
|
-
* @example
|
|
781
716
|
* ```ts
|
|
782
|
-
* const
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
* if (result.dryRun) {
|
|
786
|
-
* console.log('This was a dry run - no data persisted');
|
|
717
|
+
* const mike = await client.members.find('Mike Barker');
|
|
718
|
+
* if (mike) {
|
|
719
|
+
* console.log(mike.ratingFor('pickleball'));
|
|
787
720
|
* }
|
|
788
721
|
* ```
|
|
789
722
|
*/
|
|
790
|
-
async
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
}
|
|
794
|
-
return
|
|
723
|
+
async find(name) {
|
|
724
|
+
for await (const member of this.search({ name, pageSize: 1, maxResults: 1 })) {
|
|
725
|
+
return member;
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
795
728
|
}
|
|
796
|
-
// ---------------------------------------------------------------------------
|
|
797
|
-
// Rating Updates
|
|
798
|
-
// ---------------------------------------------------------------------------
|
|
799
729
|
/**
|
|
800
|
-
*
|
|
730
|
+
* Fetch up to 100 members by their member IDs in one call.
|
|
801
731
|
*
|
|
802
|
-
*
|
|
732
|
+
* Unknown IDs are silently omitted — the returned array may be
|
|
733
|
+
* shorter than the input. Results are returned in the same order
|
|
734
|
+
* as the input IDs.
|
|
803
735
|
*
|
|
804
|
-
* @
|
|
736
|
+
* @param ids - Array of integer member IDs (max 100).
|
|
737
|
+
* @param options - Optional filters.
|
|
738
|
+
* @param options.sport - Sport code to scope ratings (e.g. `'pickleball'`).
|
|
739
|
+
* @returns Array of {@link Member} instances.
|
|
740
|
+
* @throws {@link ValidationError} If more than 100 IDs are provided.
|
|
741
|
+
* @category Members
|
|
805
742
|
*
|
|
806
743
|
* @example
|
|
807
744
|
* ```ts
|
|
808
|
-
* const
|
|
809
|
-
* for (const
|
|
810
|
-
* console.log(
|
|
811
|
-
* if (update.improved) {
|
|
812
|
-
* const member = await update.getMember();
|
|
813
|
-
* console.log(`${member.name} improved!`);
|
|
814
|
-
* }
|
|
745
|
+
* const members = await client.members.getBulk([4873327, 4873328]);
|
|
746
|
+
* for (const m of members) {
|
|
747
|
+
* console.log(m.name, m.ratingFor('pickleball'));
|
|
815
748
|
* }
|
|
816
749
|
* ```
|
|
817
750
|
*/
|
|
818
|
-
async
|
|
819
|
-
|
|
820
|
-
"
|
|
821
|
-
|
|
822
|
-
);
|
|
823
|
-
|
|
751
|
+
async getBulk(ids, options) {
|
|
752
|
+
if (ids.length > 100) {
|
|
753
|
+
throw new ValidationError("Maximum 100 member IDs per request");
|
|
754
|
+
}
|
|
755
|
+
const query = { ids: ids.join(",") };
|
|
756
|
+
if (options?.sport) query.sport = options.sport;
|
|
757
|
+
const rows = await this.#http.request({
|
|
758
|
+
method: "GET",
|
|
759
|
+
path: "/partner/members",
|
|
760
|
+
query
|
|
761
|
+
});
|
|
762
|
+
return rows.map((row) => new Member(row));
|
|
824
763
|
}
|
|
825
764
|
/**
|
|
826
|
-
*
|
|
765
|
+
* Poll for rating change notifications.
|
|
827
766
|
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
767
|
+
* Returns a list of {@link RatingUpdate} objects for every player
|
|
768
|
+
* whose rating has changed since the last poll. Members are
|
|
769
|
+
* considered subscribed when they have an active OAuth connection
|
|
770
|
+
* with the `user:webhook:subscribe` scope.
|
|
830
771
|
*/
|
|
831
|
-
async
|
|
832
|
-
|
|
833
|
-
|
|
772
|
+
async ratingUpdates() {
|
|
773
|
+
const data = await this.#http.request({
|
|
774
|
+
method: "GET",
|
|
775
|
+
path: "/partner/rating-updates"
|
|
834
776
|
});
|
|
777
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
const updates = data.updates ?? [];
|
|
781
|
+
return updates.map((wire) => new RatingUpdate(wire));
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
function buildSearchQuery(filters, pageSize) {
|
|
785
|
+
const query = {};
|
|
786
|
+
if (filters.sport !== void 0) {
|
|
787
|
+
query.sport = Array.isArray(filters.sport) ? filters.sport.join(",") : filters.sport;
|
|
788
|
+
}
|
|
789
|
+
if (filters.memberId !== void 0) {
|
|
790
|
+
query.member = String(filters.memberId);
|
|
791
|
+
} else if (filters.name !== void 0) {
|
|
792
|
+
query.member = filters.name;
|
|
793
|
+
}
|
|
794
|
+
if (filters.city !== void 0) query.city = filters.city;
|
|
795
|
+
if (filters.state !== void 0) query.state = filters.state;
|
|
796
|
+
if (filters.country !== void 0) query.country = filters.country;
|
|
797
|
+
if (filters.zip !== void 0) query.zip = filters.zip;
|
|
798
|
+
if (filters.location !== void 0) query.location = filters.location;
|
|
799
|
+
if (filters.gender !== void 0) {
|
|
800
|
+
query.gender = filters.gender.toUpperCase();
|
|
801
|
+
}
|
|
802
|
+
if (filters.vairifiedOnly !== void 0) query.vairified = filters.vairifiedOnly;
|
|
803
|
+
if (filters.wheelchair !== void 0) query.wheelchair = filters.wheelchair;
|
|
804
|
+
if (filters.ratingMin !== void 0) query.rating1 = filters.ratingMin;
|
|
805
|
+
if (filters.ratingMax !== void 0) query.rating2 = filters.ratingMax;
|
|
806
|
+
const { ageFilterType, age1, age2 } = resolveAgeFilter(filters);
|
|
807
|
+
if (ageFilterType !== void 0) query.ageFilterType = ageFilterType;
|
|
808
|
+
if (age1 !== void 0) query.age1 = age1;
|
|
809
|
+
if (age2 !== void 0) query.age2 = age2;
|
|
810
|
+
if (filters.sortBy !== void 0) query.sortField = filters.sortBy;
|
|
811
|
+
if (filters.sortOrder !== void 0) query.sortDirection = filters.sortOrder;
|
|
812
|
+
query.limit = pageSize;
|
|
813
|
+
return query;
|
|
814
|
+
}
|
|
815
|
+
function resolveAgeFilter(filters) {
|
|
816
|
+
if (filters.age !== void 0) {
|
|
817
|
+
return { ageFilterType: "exact", age1: filters.age };
|
|
818
|
+
}
|
|
819
|
+
if (filters.ageMin !== void 0 && filters.ageMax !== void 0) {
|
|
820
|
+
return { ageFilterType: "range", age1: filters.ageMin, age2: filters.ageMax };
|
|
821
|
+
}
|
|
822
|
+
if (filters.ageMin !== void 0) {
|
|
823
|
+
return { ageFilterType: "above", age1: filters.ageMin };
|
|
824
|
+
}
|
|
825
|
+
if (filters.ageMax !== void 0) {
|
|
826
|
+
return { ageFilterType: "below", age1: filters.ageMax };
|
|
827
|
+
}
|
|
828
|
+
return {};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/oauth.ts
|
|
832
|
+
var SCOPES = Object.freeze({
|
|
833
|
+
"user:profile:read": "Access your name, location, and verification status",
|
|
834
|
+
"user:profile:email": "Access your email address",
|
|
835
|
+
"user:rating:read": "View your current rating and rating splits",
|
|
836
|
+
"user:rating:history": "View your complete rating history",
|
|
837
|
+
"user:match:submit": "Submit match results on your behalf",
|
|
838
|
+
"user:webhook:subscribe": "Receive notifications when your rating changes"
|
|
839
|
+
});
|
|
840
|
+
var DEFAULT_SCOPES = Object.freeze([
|
|
841
|
+
"user:profile:read",
|
|
842
|
+
"user:rating:read"
|
|
843
|
+
]);
|
|
844
|
+
function getAuthorizationUrl(config, options = {}) {
|
|
845
|
+
const baseUrl = (config.baseUrl ?? "https://api-next.vairified.com/api/v1").replace(/\/+$/, "");
|
|
846
|
+
const scopeList = ensureProfileRead(options.scopes ?? DEFAULT_SCOPES);
|
|
847
|
+
const params = new URLSearchParams({
|
|
848
|
+
redirect_uri: config.redirectUri,
|
|
849
|
+
scope: scopeList.join(","),
|
|
850
|
+
response_type: "code"
|
|
851
|
+
});
|
|
852
|
+
if (options.state) {
|
|
853
|
+
params.set("state", options.state);
|
|
854
|
+
}
|
|
855
|
+
return `${baseUrl}/partner/oauth/authorize?${params.toString()}`;
|
|
856
|
+
}
|
|
857
|
+
function validateScope(scope) {
|
|
858
|
+
return scope in SCOPES;
|
|
859
|
+
}
|
|
860
|
+
function describeScope(scope) {
|
|
861
|
+
if (validateScope(scope)) {
|
|
862
|
+
return SCOPES[scope];
|
|
863
|
+
}
|
|
864
|
+
return `Unknown scope: ${scope}`;
|
|
865
|
+
}
|
|
866
|
+
function describeScopes(scopes) {
|
|
867
|
+
return scopes.map((scope) => ({
|
|
868
|
+
scope,
|
|
869
|
+
description: describeScope(scope)
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
function generateState() {
|
|
873
|
+
const bytes = new Uint8Array(32);
|
|
874
|
+
crypto.getRandomValues(bytes);
|
|
875
|
+
let binary = "";
|
|
876
|
+
for (const byte of bytes) {
|
|
877
|
+
binary += String.fromCharCode(byte);
|
|
878
|
+
}
|
|
879
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
880
|
+
}
|
|
881
|
+
function ensureProfileRead(scopes) {
|
|
882
|
+
if (scopes.includes("user:profile:read")) {
|
|
883
|
+
return scopes;
|
|
884
|
+
}
|
|
885
|
+
return ["user:profile:read", ...scopes];
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/resources/oauth.ts
|
|
889
|
+
var OAuthResource = class {
|
|
890
|
+
#http;
|
|
891
|
+
/** @internal */
|
|
892
|
+
constructor(http) {
|
|
893
|
+
this.#http = http;
|
|
835
894
|
}
|
|
836
|
-
// ---------------------------------------------------------------------------
|
|
837
|
-
// OAuth Operations
|
|
838
|
-
// ---------------------------------------------------------------------------
|
|
839
895
|
/**
|
|
840
896
|
* Start an OAuth authorization flow.
|
|
841
897
|
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
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
|
|
898
|
+
* @throws {@link OAuthError} with `errorCode: 'invalid_scope'` if a
|
|
899
|
+
* requested scope is not in the accepted list.
|
|
863
900
|
*/
|
|
864
|
-
async
|
|
865
|
-
const
|
|
866
|
-
scopeSet.add("profile:read");
|
|
867
|
-
const scopeList = Array.from(scopeSet);
|
|
901
|
+
async authorize(options) {
|
|
902
|
+
const scopeList = ensureProfileRead(options.scopes ?? DEFAULT_SCOPES);
|
|
868
903
|
for (const scope of scopeList) {
|
|
869
904
|
if (!(scope in SCOPES)) {
|
|
870
905
|
throw new OAuthError(`Invalid scope: ${scope}`, "invalid_scope");
|
|
871
906
|
}
|
|
872
907
|
}
|
|
873
|
-
const data = await this.request(
|
|
908
|
+
const data = await this.#http.request({
|
|
909
|
+
method: "POST",
|
|
910
|
+
path: "/partner/oauth/authorize",
|
|
874
911
|
body: {
|
|
875
|
-
redirectUri,
|
|
912
|
+
redirectUri: options.redirectUri,
|
|
876
913
|
scope: scopeList.join(","),
|
|
877
|
-
state
|
|
914
|
+
state: options.state
|
|
878
915
|
}
|
|
879
916
|
});
|
|
880
917
|
return {
|
|
881
|
-
authorizationUrl: data
|
|
882
|
-
code: data
|
|
883
|
-
state
|
|
918
|
+
authorizationUrl: data?.authorizationUrl ?? "",
|
|
919
|
+
code: data?.code ?? "",
|
|
920
|
+
state: options.state
|
|
884
921
|
};
|
|
885
922
|
}
|
|
886
|
-
/**
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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 }
|
|
923
|
+
/** Exchange an authorization code for access and refresh tokens. */
|
|
924
|
+
async exchangeToken(options) {
|
|
925
|
+
const data = await this.#http.request({
|
|
926
|
+
method: "POST",
|
|
927
|
+
path: "/partner/oauth/token",
|
|
928
|
+
body: { code: options.code, redirectUri: options.redirectUri }
|
|
913
929
|
});
|
|
914
|
-
return
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
930
|
+
return tokenResponseFromWire(data);
|
|
931
|
+
}
|
|
932
|
+
/** Refresh an expired access token using a refresh token. */
|
|
933
|
+
async refresh(refreshToken) {
|
|
934
|
+
const data = await this.#http.request({
|
|
935
|
+
method: "POST",
|
|
936
|
+
path: "/partner/oauth/refresh",
|
|
937
|
+
body: { refreshToken }
|
|
938
|
+
});
|
|
939
|
+
return tokenResponseFromWire(data);
|
|
940
|
+
}
|
|
941
|
+
/** Revoke a player's OAuth connection to your app. */
|
|
942
|
+
async revoke(playerId) {
|
|
943
|
+
const data = await this.#http.request({
|
|
944
|
+
method: "POST",
|
|
945
|
+
path: "/partner/oauth/revoke",
|
|
946
|
+
body: { playerId }
|
|
947
|
+
});
|
|
948
|
+
return data ?? {};
|
|
949
|
+
}
|
|
950
|
+
/** Return the list of OAuth scopes the server currently supports. */
|
|
951
|
+
async availableScopes() {
|
|
952
|
+
const data = await this.#http.request({
|
|
953
|
+
method: "GET",
|
|
954
|
+
path: "/partner/oauth/scopes"
|
|
955
|
+
});
|
|
956
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
957
|
+
return [];
|
|
958
|
+
}
|
|
959
|
+
return data.scopes ?? [];
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
function tokenResponseFromWire(data) {
|
|
963
|
+
const scopeRaw = data?.scope ?? "";
|
|
964
|
+
const scopeList = scopeRaw.length > 0 ? scopeRaw.split(",") : [];
|
|
965
|
+
return {
|
|
966
|
+
accessToken: data?.accessToken ?? "",
|
|
967
|
+
refreshToken: data?.refreshToken ?? null,
|
|
968
|
+
expiresIn: data?.expiresIn ?? 3600,
|
|
969
|
+
scope: Object.freeze(scopeList),
|
|
970
|
+
playerId: data?.playerId ?? ""
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/models/webhook-delivery.ts
|
|
975
|
+
var WebhookDelivery = class {
|
|
976
|
+
id;
|
|
977
|
+
event;
|
|
978
|
+
url;
|
|
979
|
+
statusCode;
|
|
980
|
+
responseBody;
|
|
981
|
+
errorMessage;
|
|
982
|
+
attempts;
|
|
983
|
+
maxAttempts;
|
|
984
|
+
lastAttemptAt;
|
|
985
|
+
nextRetryAt;
|
|
986
|
+
completedAt;
|
|
987
|
+
createdAt;
|
|
988
|
+
payload;
|
|
989
|
+
/** @internal */
|
|
990
|
+
constructor(wire) {
|
|
991
|
+
this.id = wire.id;
|
|
992
|
+
this.event = wire.event;
|
|
993
|
+
this.url = wire.url;
|
|
994
|
+
this.statusCode = wire.statusCode;
|
|
995
|
+
this.responseBody = wire.responseBody;
|
|
996
|
+
this.errorMessage = wire.errorMessage;
|
|
997
|
+
this.attempts = wire.attempts;
|
|
998
|
+
this.maxAttempts = wire.maxAttempts;
|
|
999
|
+
this.lastAttemptAt = wire.lastAttemptAt;
|
|
1000
|
+
this.nextRetryAt = wire.nextRetryAt;
|
|
1001
|
+
this.completedAt = wire.completedAt;
|
|
1002
|
+
this.createdAt = wire.createdAt;
|
|
1003
|
+
this.payload = Object.freeze({ ...wire.payload });
|
|
1004
|
+
Object.freeze(this);
|
|
1005
|
+
}
|
|
1006
|
+
/** Whether delivery completed successfully (2xx status). */
|
|
1007
|
+
get succeeded() {
|
|
1008
|
+
return this.completedAt != null && this.statusCode != null && this.statusCode >= 200 && this.statusCode < 300;
|
|
1009
|
+
}
|
|
1010
|
+
/** Whether delivery failed definitively (completed with non-2xx). */
|
|
1011
|
+
get failed() {
|
|
1012
|
+
return this.completedAt != null && !this.succeeded;
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
var WebhookDeliveriesResult = class {
|
|
1016
|
+
deliveries;
|
|
1017
|
+
total;
|
|
1018
|
+
/** @internal */
|
|
1019
|
+
constructor(wire) {
|
|
1020
|
+
this.deliveries = Object.freeze(wire.deliveries.map((d) => new WebhookDelivery(d)));
|
|
1021
|
+
this.total = wire.total;
|
|
1022
|
+
Object.freeze(this);
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
// src/resources/webhooks.ts
|
|
1027
|
+
var WebhooksResource = class {
|
|
1028
|
+
#http;
|
|
1029
|
+
/** @internal */
|
|
1030
|
+
constructor(http) {
|
|
1031
|
+
this.#http = http;
|
|
921
1032
|
}
|
|
922
1033
|
/**
|
|
923
|
-
*
|
|
1034
|
+
* List recent webhook delivery attempts.
|
|
924
1035
|
*
|
|
925
|
-
*
|
|
926
|
-
*
|
|
927
|
-
*
|
|
928
|
-
* @param
|
|
929
|
-
* @
|
|
930
|
-
* @
|
|
1036
|
+
* @param options - Optional filters and pagination.
|
|
1037
|
+
* @param options.event - Filter by event type (e.g. `'rating.updated'`).
|
|
1038
|
+
* @param options.status - Filter: `'all'`, `'pending'`, `'success'`, or `'failed'`.
|
|
1039
|
+
* @param options.limit - Results per page (1-100, default 20).
|
|
1040
|
+
* @param options.offset - Pagination offset.
|
|
1041
|
+
* @returns {@link WebhookDeliveriesResult} with entries and total.
|
|
1042
|
+
* @category Webhooks
|
|
931
1043
|
*
|
|
932
1044
|
* @example
|
|
933
1045
|
* ```ts
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
* } catch (e) {
|
|
938
|
-
* if (e instanceof OAuthError && e.errorCode === 'invalid_grant') {
|
|
939
|
-
* // Refresh token revoked, user needs to re-authorize
|
|
940
|
-
* }
|
|
1046
|
+
* const result = await client.webhooks.deliveries({ status: 'failed' });
|
|
1047
|
+
* for (const d of result.deliveries) {
|
|
1048
|
+
* console.log(d.event, d.statusCode, d.errorMessage);
|
|
941
1049
|
* }
|
|
942
1050
|
* ```
|
|
943
|
-
*
|
|
944
|
-
* @category OAuth
|
|
945
1051
|
*/
|
|
946
|
-
async
|
|
947
|
-
const
|
|
948
|
-
|
|
1052
|
+
async deliveries(options) {
|
|
1053
|
+
const query = {};
|
|
1054
|
+
if (options?.event) query.event = options.event;
|
|
1055
|
+
if (options?.status) query.status = options.status;
|
|
1056
|
+
if (options?.limit != null) query.limit = options.limit;
|
|
1057
|
+
if (options?.offset != null) query.offset = options.offset;
|
|
1058
|
+
const data = await this.#http.request({
|
|
1059
|
+
method: "GET",
|
|
1060
|
+
path: "/partner/webhook-deliveries",
|
|
1061
|
+
query
|
|
949
1062
|
});
|
|
950
|
-
return
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1063
|
+
return new WebhookDeliveriesResult(data);
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
// src/client.ts
|
|
1068
|
+
var ENVIRONMENTS = Object.freeze({
|
|
1069
|
+
production: "https://api-next.vairified.com/api/v1",
|
|
1070
|
+
staging: "https://api-staging.vairified.com/api/v1",
|
|
1071
|
+
local: "http://localhost:3001/api/v1"
|
|
1072
|
+
});
|
|
1073
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1074
|
+
var Vairified = class {
|
|
1075
|
+
/** The resolved API key this client is using. */
|
|
1076
|
+
apiKey;
|
|
1077
|
+
/** The resolved base URL (production, staging, local, or custom). */
|
|
1078
|
+
baseUrl;
|
|
1079
|
+
/** The resolved environment name. */
|
|
1080
|
+
env;
|
|
1081
|
+
/** Request timeout in milliseconds. */
|
|
1082
|
+
timeoutMs;
|
|
1083
|
+
/** Member operations — get, search, find, ratingUpdates. */
|
|
1084
|
+
members;
|
|
1085
|
+
/** Match submission — submit, testWebhook. */
|
|
1086
|
+
matches;
|
|
1087
|
+
/** OAuth flow — authorize, exchangeToken, refresh, revoke. */
|
|
1088
|
+
oauth;
|
|
1089
|
+
/** Leaderboard queries — list, rank, categories. */
|
|
1090
|
+
leaderboard;
|
|
1091
|
+
/** Webhook delivery inspection — deliveries. */
|
|
1092
|
+
webhooks;
|
|
1093
|
+
#transport;
|
|
1094
|
+
constructor(options = {}) {
|
|
1095
|
+
const apiKey = options.apiKey ?? process.env.VAIRIFIED_API_KEY ?? "";
|
|
1096
|
+
if (apiKey.length === 0) {
|
|
1097
|
+
throw new Error("API key required. Pass { apiKey } or set VAIRIFIED_API_KEY.");
|
|
1098
|
+
}
|
|
1099
|
+
this.apiKey = apiKey;
|
|
1100
|
+
if (options.baseUrl) {
|
|
1101
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
1102
|
+
this.env = options.env ?? "production";
|
|
1103
|
+
} else {
|
|
1104
|
+
const envName = options.env ?? process.env.VAIRIFIED_ENV ?? "production";
|
|
1105
|
+
if (options.env && !(envName in ENVIRONMENTS)) {
|
|
1106
|
+
throw new Error(
|
|
1107
|
+
`Unknown environment: ${envName}. Use one of: ${Object.keys(ENVIRONMENTS).join(", ")}`
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
this.env = envName;
|
|
1111
|
+
this.baseUrl = ENVIRONMENTS[envName] ?? ENVIRONMENTS.production;
|
|
1112
|
+
}
|
|
1113
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1114
|
+
this.#transport = new HttpTransport({
|
|
1115
|
+
baseUrl: this.baseUrl,
|
|
1116
|
+
apiKey: this.apiKey,
|
|
1117
|
+
timeoutMs: this.timeoutMs,
|
|
1118
|
+
fetch: options.fetch ?? fetch
|
|
1119
|
+
});
|
|
1120
|
+
this.members = new MembersResource(this.#transport);
|
|
1121
|
+
this.matches = new MatchesResource(this.#transport);
|
|
1122
|
+
this.oauth = new OAuthResource(this.#transport);
|
|
1123
|
+
this.leaderboard = new LeaderboardResource(this.#transport);
|
|
1124
|
+
this.webhooks = new WebhooksResource(this.#transport);
|
|
957
1125
|
}
|
|
958
1126
|
/**
|
|
959
|
-
*
|
|
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.
|
|
1127
|
+
* API usage statistics for the current API key.
|
|
963
1128
|
*
|
|
964
|
-
*
|
|
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
|
-
* ```
|
|
972
|
-
*
|
|
973
|
-
* @category OAuth
|
|
1129
|
+
* Returns rate-limit status, request counts, and quota usage.
|
|
974
1130
|
*/
|
|
975
|
-
async
|
|
976
|
-
await this.request(
|
|
977
|
-
|
|
1131
|
+
async usage() {
|
|
1132
|
+
const data = await this.#transport.request({
|
|
1133
|
+
method: "GET",
|
|
1134
|
+
path: "/partner/usage"
|
|
978
1135
|
});
|
|
1136
|
+
return data ?? {};
|
|
979
1137
|
}
|
|
980
1138
|
/**
|
|
981
|
-
*
|
|
1139
|
+
* Release any resources held by the client.
|
|
982
1140
|
*
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
* ```ts
|
|
987
|
-
* const scopes = await client.getAvailableScopes();
|
|
988
|
-
* for (const scope of scopes) {
|
|
989
|
-
* console.log(`${scope.id}: ${scope.description}`);
|
|
990
|
-
* }
|
|
991
|
-
* ```
|
|
992
|
-
*
|
|
993
|
-
* @category OAuth
|
|
1141
|
+
* The current transport is stateless, so this is a no-op today, but
|
|
1142
|
+
* partners should still call it (or use `await using`) so the SDK
|
|
1143
|
+
* can add connection pooling later without breaking them.
|
|
994
1144
|
*/
|
|
995
|
-
async
|
|
996
|
-
const data = await this.request("GET", "/partner/oauth/scopes");
|
|
997
|
-
return data.scopes ?? [];
|
|
1145
|
+
async close() {
|
|
998
1146
|
}
|
|
999
1147
|
/**
|
|
1000
|
-
*
|
|
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
|
|
1148
|
+
* Explicit resource management hook — enables
|
|
1149
|
+
* `await using client = new Vairified({ ... })` (TypeScript 5.2+).
|
|
1012
1150
|
*/
|
|
1013
|
-
async
|
|
1014
|
-
|
|
1151
|
+
async [Symbol.asyncDispose]() {
|
|
1152
|
+
await this.close();
|
|
1153
|
+
}
|
|
1154
|
+
/** Compact summary for console output. */
|
|
1155
|
+
toString() {
|
|
1156
|
+
return `Vairified { env: '${this.env}', baseUrl: '${this.baseUrl}' }`;
|
|
1015
1157
|
}
|
|
1016
1158
|
};
|
|
1017
1159
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1018
1160
|
0 && (module.exports = {
|
|
1019
1161
|
AuthenticationError,
|
|
1020
1162
|
DEFAULT_SCOPES,
|
|
1021
|
-
|
|
1022
|
-
|
|
1163
|
+
ENVIRONMENTS,
|
|
1164
|
+
LeaderboardResource,
|
|
1165
|
+
MatchBatchResult,
|
|
1166
|
+
MatchesResource,
|
|
1023
1167
|
Member,
|
|
1168
|
+
MemberSportMap,
|
|
1169
|
+
MembersResource,
|
|
1024
1170
|
NotFoundError,
|
|
1025
1171
|
OAuthError,
|
|
1026
|
-
|
|
1172
|
+
OAuthResource,
|
|
1027
1173
|
RateLimitError,
|
|
1028
|
-
RatingSplit,
|
|
1029
|
-
RatingSplits,
|
|
1030
1174
|
RatingUpdate,
|
|
1031
1175
|
SCOPES,
|
|
1032
|
-
|
|
1176
|
+
SportRating,
|
|
1177
|
+
TournamentImportResult,
|
|
1033
1178
|
Vairified,
|
|
1034
1179
|
VairifiedError,
|
|
1035
1180
|
ValidationError,
|
|
1181
|
+
WebhookDeliveriesResult,
|
|
1182
|
+
WebhookDelivery,
|
|
1183
|
+
WebhooksResource,
|
|
1036
1184
|
describeScope,
|
|
1037
1185
|
describeScopes,
|
|
1038
1186
|
generateState,
|