vairified 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,13 +50,14 @@ exits. If you can't use it, call `await client.close()` manually.
50
50
 
51
51
  Every operation lives on a sub-resource that mirrors the REST path:
52
52
 
53
- | Sub-resource | Operations |
54
- |-------------------------|-------------------------------------------------------------|
55
- | `client.members` | `get`, `search`, `find`, `ratingUpdates` |
56
- | `client.matches` | `submit`, `testWebhook` |
57
- | `client.oauth` | `authorize`, `exchangeToken`, `refresh`, `revoke` |
58
- | `client.leaderboard` | `list`, `rank`, `categories` |
59
- | `client.usage()` | Rate-limit + request-count stats |
53
+ | Sub-resource | Operations |
54
+ |-------------------------|------------------------------------------------------------------|
55
+ | `client.members` | `get`, `getBulk`, `search`, `find`, `ratingUpdates` |
56
+ | `client.matches` | `submit`, `tournamentImport`, `testWebhook` |
57
+ | `client.oauth` | `authorize`, `exchangeToken`, `refresh`, `revoke` |
58
+ | `client.webhooks` | `deliveries` |
59
+ | `client.leaderboard` | `list`, `rank`, `categories` |
60
+ | `client.usage()` | Rate-limit + request-count stats |
60
61
 
61
62
  ## Members
62
63
 
@@ -82,6 +83,22 @@ if (pb) {
82
83
  }
83
84
  ```
84
85
 
86
+ ### Bulk member lookup
87
+
88
+ Fetch up to 100 members in one call by their integer member IDs:
89
+
90
+ ```ts
91
+ const members = await client.members.getBulk([4873327, 4873328, 4873329]);
92
+ for (const m of members) {
93
+ console.log(m.name, m.ratingFor('pickleball'));
94
+ }
95
+
96
+ // Filter to a specific sport
97
+ const pb = await client.members.getBulk([4873327], { sport: 'pickleball' });
98
+ ```
99
+
100
+ Unknown IDs are silently omitted — the returned array may be shorter than the input.
101
+
85
102
  ### Filter ratings to specific sports
86
103
 
87
104
  ```ts
@@ -172,8 +189,57 @@ if (result.ok) {
172
189
  }
173
190
  ```
174
191
 
175
- Set `dryRun: true` on the batch to validate without persisting your API key must have
176
- the `dry-run` scope.
192
+ Set `dryRun: true` on the batch to validate without persisting. No special scope
193
+ needed — any key with `key:match:submit` can dry-run.
194
+
195
+ ### Tournament import
196
+
197
+ Import historical tournament results with automatic player matching. Unmatched players
198
+ become ghost accounts that can be claimed later.
199
+
200
+ ```ts
201
+ const result = await client.matches.tournamentImport({
202
+ tournamentName: 'Austin Open 2026',
203
+ sport: 'pickleball',
204
+ winScore: 11,
205
+ winBy: 2,
206
+ matches: [
207
+ {
208
+ identifier: 'USAP-R1-M1',
209
+ event: 'Austin Open',
210
+ bracket: "Men's Pro Doubles",
211
+ format: 'DOUBLES',
212
+ matchDate: '2026-04-11T10:00:00Z',
213
+ teamA: {
214
+ player1: { firstName: 'Ben', lastName: 'Johns' },
215
+ player2: { firstName: 'Matt', lastName: 'Wright' },
216
+ game1: 11, game2: 11,
217
+ },
218
+ teamB: {
219
+ player1: { firstName: 'JW', lastName: 'Johnson' },
220
+ player2: { firstName: 'Dylan', lastName: 'Frazier' },
221
+ game1: 7, game2: 9,
222
+ },
223
+ },
224
+ ],
225
+ });
226
+ console.log(`Imported ${result.matchesImported} matches, ${result.ghostPlayersCreated} ghosts`);
227
+ ```
228
+
229
+ ## Webhook Deliveries
230
+
231
+ Inspect recent webhook delivery attempts for your app:
232
+
233
+ ```ts
234
+ const result = await client.webhooks.deliveries({ status: 'failed', limit: 10 });
235
+ for (const d of result.deliveries) {
236
+ console.log(d.event, d.statusCode, d.errorMessage);
237
+ }
238
+
239
+ // Filter by event type
240
+ const ratingEvents = await client.webhooks.deliveries({ event: 'rating.updated' });
241
+ console.log(`${ratingEvents.total} total rating.updated deliveries`);
242
+ ```
177
243
 
178
244
  ## OAuth Connect Flow
179
245
 
@@ -186,7 +252,7 @@ await using client = new Vairified({ apiKey: 'vair_pk_xxx' });
186
252
  const state = generateState();
187
253
  const auth = await client.oauth.authorize({
188
254
  redirectUri: 'https://myapp.com/oauth/callback',
189
- scopes: ['profile:read', 'rating:read', 'match:submit'],
255
+ scopes: ['user:profile:read', 'user:rating:read', 'user:match:submit'],
190
256
  state,
191
257
  });
192
258
  // Redirect user to auth.authorizationUrl
@@ -216,20 +282,20 @@ await client.oauth.revoke(playerId);
216
282
  ```ts
217
283
  import type { OAuthScope } from 'vairified';
218
284
 
219
- const scopes: OAuthScope[] = ['profile:read', 'rating:read']; // ok
220
- const bad: OAuthScope[] = ['profile:read', 'rating']; // type error
285
+ const scopes: OAuthScope[] = ['user:profile:read', 'user:rating:read']; // ok
286
+ const bad: OAuthScope[] = ['user:profile:read', 'rating']; // type error
221
287
  ```
222
288
 
223
289
  ### Available scopes
224
290
 
225
- | Scope | Description |
226
- |----------------------|------------------------------------------------|
227
- | `profile:read` | Name, location, verification status |
228
- | `profile:email` | Email address |
229
- | `rating:read` | Current rating and rating splits |
230
- | `rating:history` | Complete rating history |
231
- | `match:submit` | Submit matches on behalf of user |
232
- | `webhook:subscribe` | Rating change notifications |
291
+ | Scope | Description |
292
+ |---------------------------|------------------------------------------------|
293
+ | `user:profile:read` | Name, location, verification status |
294
+ | `user:profile:email` | Email address |
295
+ | `user:rating:read` | Current rating and rating splits |
296
+ | `user:rating:history` | Complete rating history |
297
+ | `user:match:submit` | Submit matches on behalf of user |
298
+ | `user:webhook:subscribe` | Rating change notifications |
233
299
 
234
300
  ## Leaderboards
235
301
 
@@ -364,9 +430,13 @@ pb?.has('singles-40+') // Membership check
364
430
  for (const [key, split] of pb ?? []) { /* iterate */ }
365
431
  ```
366
432
 
367
- ## Migrating from 0.1.x
433
+ ## Migrating
434
+
435
+ **From 0.2.x → 0.3.0:** All OAuth scope strings gained a `user:` prefix
436
+ (`profile:read` → `user:profile:read`). Update any hardcoded scope arrays.
437
+ New: `members.getBulk()`, `matches.tournamentImport()`, `webhooks.deliveries()`.
368
438
 
369
- Version 0.2.0 is a breaking rewrite. See the
439
+ **From 0.1.x → 0.2.0:** Full rewrite. See the
370
440
  [migration guide](https://vairified.github.io/vairified.js/documents/Migrating_from_0.1.x.html)
371
441
  for the full diff and [CHANGELOG.md](CHANGELOG.md) for the release notes.
372
442
 
package/dist/index.cjs CHANGED
@@ -36,9 +36,13 @@ __export(index_exports, {
36
36
  RatingUpdate: () => RatingUpdate,
37
37
  SCOPES: () => SCOPES,
38
38
  SportRating: () => SportRating,
39
+ TournamentImportResult: () => TournamentImportResult,
39
40
  Vairified: () => Vairified,
40
41
  VairifiedError: () => VairifiedError,
41
42
  ValidationError: () => ValidationError,
43
+ WebhookDeliveriesResult: () => WebhookDeliveriesResult,
44
+ WebhookDelivery: () => WebhookDelivery,
45
+ WebhooksResource: () => WebhooksResource,
42
46
  describeScope: () => describeScope,
43
47
  describeScopes: () => describeScopes,
44
48
  generateState: () => generateState,
@@ -293,6 +297,34 @@ var MatchBatchResult = class {
293
297
  }
294
298
  };
295
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. */
323
+ get ok() {
324
+ return this.success && this.errors.length === 0;
325
+ }
326
+ };
327
+
296
328
  // src/resources/matches.ts
297
329
  var MatchesResource = class {
298
330
  #http;
@@ -303,9 +335,9 @@ var MatchesResource = class {
303
335
  /**
304
336
  * Submit a {@link MatchBatch} for rating calculation.
305
337
  *
306
- * All players in every match must have granted the `match:submit`
338
+ * All players in every match must have granted the `user:match:submit`
307
339
  * scope via OAuth (unless your API key has the
308
- * `match:submit:trusted` scope, which skips per-player consent).
340
+ * `user:match:submit:trusted` scope, which skips per-player consent).
309
341
  *
310
342
  * Set `batch.dryRun = true` to validate without persisting.
311
343
  *
@@ -339,6 +371,37 @@ var MatchesResource = class {
339
371
  });
340
372
  return new MatchBatchResult(wire);
341
373
  }
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);
404
+ }
342
405
  /** Send a test payload to a webhook URL. */
343
406
  async testWebhook(webhookUrl) {
344
407
  const data = await this.#http.request({
@@ -663,13 +726,48 @@ var MembersResource = class {
663
726
  }
664
727
  return null;
665
728
  }
729
+ /**
730
+ * Fetch up to 100 members by their member IDs in one call.
731
+ *
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.
735
+ *
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
742
+ *
743
+ * @example
744
+ * ```ts
745
+ * const members = await client.members.getBulk([4873327, 4873328]);
746
+ * for (const m of members) {
747
+ * console.log(m.name, m.ratingFor('pickleball'));
748
+ * }
749
+ * ```
750
+ */
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));
763
+ }
666
764
  /**
667
765
  * Poll for rating change notifications.
668
766
  *
669
767
  * Returns a list of {@link RatingUpdate} objects for every player
670
768
  * whose rating has changed since the last poll. Members are
671
769
  * considered subscribed when they have an active OAuth connection
672
- * with the `webhook:subscribe` scope.
770
+ * with the `user:webhook:subscribe` scope.
673
771
  */
674
772
  async ratingUpdates() {
675
773
  const data = await this.#http.request({
@@ -732,14 +830,17 @@ function resolveAgeFilter(filters) {
732
830
 
733
831
  // src/oauth.ts
734
832
  var SCOPES = Object.freeze({
735
- "profile:read": "Access your name, location, and verification status",
736
- "profile:email": "Access your email address",
737
- "rating:read": "View your current rating and rating splits",
738
- "rating:history": "View your complete rating history",
739
- "match:submit": "Submit match results on your behalf",
740
- "webhook:subscribe": "Receive notifications when your rating changes"
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"
741
839
  });
742
- var DEFAULT_SCOPES = Object.freeze(["profile:read", "rating:read"]);
840
+ var DEFAULT_SCOPES = Object.freeze([
841
+ "user:profile:read",
842
+ "user:rating:read"
843
+ ]);
743
844
  function getAuthorizationUrl(config, options = {}) {
744
845
  const baseUrl = (config.baseUrl ?? "https://api-next.vairified.com/api/v1").replace(/\/+$/, "");
745
846
  const scopeList = ensureProfileRead(options.scopes ?? DEFAULT_SCOPES);
@@ -778,10 +879,10 @@ function generateState() {
778
879
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
779
880
  }
780
881
  function ensureProfileRead(scopes) {
781
- if (scopes.includes("profile:read")) {
882
+ if (scopes.includes("user:profile:read")) {
782
883
  return scopes;
783
884
  }
784
- return ["profile:read", ...scopes];
885
+ return ["user:profile:read", ...scopes];
785
886
  }
786
887
 
787
888
  // src/resources/oauth.ts
@@ -870,6 +971,99 @@ function tokenResponseFromWire(data) {
870
971
  };
871
972
  }
872
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;
1032
+ }
1033
+ /**
1034
+ * List recent webhook delivery attempts.
1035
+ *
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
1043
+ *
1044
+ * @example
1045
+ * ```ts
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);
1049
+ * }
1050
+ * ```
1051
+ */
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
1062
+ });
1063
+ return new WebhookDeliveriesResult(data);
1064
+ }
1065
+ };
1066
+
873
1067
  // src/client.ts
874
1068
  var ENVIRONMENTS = Object.freeze({
875
1069
  production: "https://api-next.vairified.com/api/v1",
@@ -894,6 +1088,8 @@ var Vairified = class {
894
1088
  oauth;
895
1089
  /** Leaderboard queries — list, rank, categories. */
896
1090
  leaderboard;
1091
+ /** Webhook delivery inspection — deliveries. */
1092
+ webhooks;
897
1093
  #transport;
898
1094
  constructor(options = {}) {
899
1095
  const apiKey = options.apiKey ?? process.env.VAIRIFIED_API_KEY ?? "";
@@ -925,6 +1121,7 @@ var Vairified = class {
925
1121
  this.matches = new MatchesResource(this.#transport);
926
1122
  this.oauth = new OAuthResource(this.#transport);
927
1123
  this.leaderboard = new LeaderboardResource(this.#transport);
1124
+ this.webhooks = new WebhooksResource(this.#transport);
928
1125
  }
929
1126
  /**
930
1127
  * API usage statistics for the current API key.
@@ -977,9 +1174,13 @@ var Vairified = class {
977
1174
  RatingUpdate,
978
1175
  SCOPES,
979
1176
  SportRating,
1177
+ TournamentImportResult,
980
1178
  Vairified,
981
1179
  VairifiedError,
982
1180
  ValidationError,
1181
+ WebhookDeliveriesResult,
1182
+ WebhookDelivery,
1183
+ WebhooksResource,
983
1184
  describeScope,
984
1185
  describeScopes,
985
1186
  generateState,