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 CHANGED
@@ -1,18 +1,22 @@
1
- <h1 align="center">Vairified JavaScript SDK</h1>
1
+ <h1 align="center">Vairified TypeScript SDK</h1>
2
2
 
3
3
  <p align="center">
4
4
  <strong>Official TypeScript/JavaScript SDK for the Vairified Partner API</strong><br>
5
- Player ratings, search, and match submission
5
+ Multi-sport player ratings, search, and bulk match submission
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
9
  <a href="https://github.com/Vairified/vairified.js/actions/workflows/ci.yml"><img src="https://github.com/Vairified/vairified.js/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
10
10
  <a href="https://www.npmjs.com/package/vairified"><img src="https://img.shields.io/npm/v/vairified.svg" alt="npm"></a>
11
+ <img src="https://img.shields.io/badge/node-%3E%3D24-blue.svg" alt="Node.js 24+">
12
+ <img src="https://img.shields.io/badge/deps-0-green.svg" alt="Zero dependencies">
11
13
  </p>
12
14
 
13
15
  ---
14
16
 
15
- TypeScript/JavaScript SDK for integrating with the [Vairified](https://vairified.com) player rating platform. Zero dependencies, works in Node.js and browsers.
17
+ Async-first TypeScript SDK for the [Vairified](https://vairified.com) Partner API. Built on
18
+ native `fetch` with **zero runtime dependencies**. Sub-resource layout, auto-paginating search,
19
+ n-team × n-game match submission, and `await using` lifecycle support on Node 24+.
16
20
 
17
21
  ## Installation
18
22
 
@@ -20,7 +24,7 @@ TypeScript/JavaScript SDK for integrating with the [Vairified](https://vairified
20
24
  npm install vairified
21
25
  ```
22
26
 
23
- Or with other package managers:
27
+ Or with any other package manager:
24
28
 
25
29
  ```bash
26
30
  yarn add vairified
@@ -30,353 +34,412 @@ bun add vairified
30
34
 
31
35
  ## Quick Start
32
36
 
33
- ```typescript
37
+ ```ts
34
38
  import { Vairified } from 'vairified';
35
39
 
36
- const client = new Vairified({ apiKey: 'vair_pk_xxx' });
40
+ await using client = new Vairified({ apiKey: 'vair_pk_xxx' });
37
41
 
38
- // Get a member - automatically subscribes to their rating updates
39
- const member = await client.getMember('clerk_user_123');
40
- console.log(`${member.name}: ${member.rating}`);
41
- console.log(`Verified: ${member.isVairified}`);
42
- console.log(`Best rating: ${member.ratingSplits.best}`);
42
+ const member = await client.members.get('vair_mem_xxx');
43
+ console.log(member.name, 'rated', member.ratingFor('pickleball'));
43
44
  ```
44
45
 
45
- ## Features
46
+ `await using` (TypeScript 5.2+, Node 20+) closes the client deterministically when the block
47
+ exits. If you can't use it, call `await client.close()` manually.
46
48
 
47
- ### Search for Players
49
+ ## Sub-resources
48
50
 
49
- ```typescript
50
- const client = new Vairified({ apiKey: 'vair_pk_xxx' });
51
+ Every operation lives on a sub-resource that mirrors the REST path:
51
52
 
52
- // Search with filters
53
- const results = await client.search({
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 |
61
+
62
+ ## Members
63
+
64
+ ### Get a connected member
65
+
66
+ ```ts
67
+ const member = await client.members.get('vair_mem_xxx');
68
+
69
+ console.log(member.name); // Full name
70
+ console.log(member.displayName); // "Mike B."
71
+ console.log(member.ratingFor('pickleball')); // 3.915
72
+ console.log(member.status.isVairified); // true
73
+
74
+ // Dict-like access to rating splits for a specific sport
75
+ const pb = member.sport.get('pickleball');
76
+ if (pb) {
77
+ console.log(pb.rating, pb.abbr); // 3.915 VO
78
+ console.log(pb.get('overall-open')?.rating); // 3.915
79
+ console.log(pb.has('singles-open')); // true
80
+ for (const [key, split] of pb) {
81
+ console.log(key, split.rating);
82
+ }
83
+ }
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
+
102
+ ### Filter ratings to specific sports
103
+
104
+ ```ts
105
+ // Just pickleball
106
+ const member = await client.members.get('vair_mem_xxx', { sport: 'pickleball' });
107
+
108
+ // Multiple sports
109
+ const member2 = await client.members.get('vair_mem_xxx', {
110
+ sport: ['pickleball', 'padel'],
111
+ });
112
+ ```
113
+
114
+ ### Auto-paginating search
115
+
116
+ `search()` is an async generator — iterate directly with `for await`. Pages are fetched
117
+ lazily, so memory usage stays bounded regardless of result count.
118
+
119
+ ```ts
120
+ for await (const member of client.members.search({
54
121
  city: 'Austin',
55
122
  state: 'TX',
56
123
  ratingMin: 3.5,
57
124
  ratingMax: 4.5,
58
125
  vairifiedOnly: true,
59
- limit: 20,
60
- });
61
-
62
- // Iterate over results
63
- for (const player of results) {
64
- console.log(`${player.name}: ${player.rating}`);
126
+ })) {
127
+ console.log(member.name, member.ratingFor('pickleball'));
65
128
  }
66
129
 
67
- // Pagination
68
- console.log(`Page ${results.page} of ${results.pages}`);
69
- if (results.hasMore) {
70
- const nextPage = await results.nextPage();
130
+ // Cap with maxResults, or break out early
131
+ const top20: Member[] = [];
132
+ for await (const m of client.members.search({ name: 'Smith', maxResults: 20 })) {
133
+ top20.push(m);
71
134
  }
72
135
  ```
73
136
 
74
- ### Find a Player by Name
137
+ ### Find by name (first hit only)
75
138
 
76
- ```typescript
77
- const player = await client.findPlayer('John Smith');
78
- if (player) {
79
- console.log(`Found: ${player.name} (${player.rating})`);
139
+ ```ts
140
+ const mike = await client.members.find('Mike Barker');
141
+ if (mike) {
142
+ console.log(mike.ratingFor('pickleball'));
80
143
  }
81
144
  ```
82
145
 
83
- ### Submit Match Results
146
+ ### Rating change notifications
84
147
 
85
- ```typescript
86
- import { Vairified, Match } from 'vairified';
148
+ ```ts
149
+ const updates = await client.members.ratingUpdates();
150
+ for (const update of updates) {
151
+ const arrow = update.improved ? '↑' : '↓';
152
+ console.log(`${update.displayName} ${arrow} delta=${update.delta?.toFixed(3)}`);
153
+ }
154
+ ```
87
155
 
88
- const client = new Vairified({ apiKey: 'vair_pk_xxx' });
156
+ ## Match Submission
89
157
 
90
- // Doubles match: 11-9, 11-7
91
- const match = new Match({
92
- event: 'Weekly League',
158
+ Matches are submitted as a `MatchBatch` — defaults at the batch level apply to every
159
+ match unless overridden. The shape is n-team × n-game, so singles, doubles, and
160
+ round-robin all go through the same path.
161
+
162
+ ```ts
163
+ const result = await client.matches.submit({
164
+ sport: 'pickleball',
165
+ winScore: 11,
166
+ winBy: 2,
93
167
  bracket: '4.0 Doubles',
94
- date: new Date(),
95
- team1: ['player1_id', 'player2_id'],
96
- team2: ['player3_id', 'player4_id'],
97
- scores: [[11, 9], [11, 7]],
168
+ event: 'Weekly League',
169
+ matchDate: '2026-04-11T14:00:00Z',
170
+ matches: [
171
+ {
172
+ identifier: 'm1',
173
+ teams: [
174
+ ['vair_mem_aaa', 'vair_mem_bbb'],
175
+ ['vair_mem_ccc', 'vair_mem_ddd'],
176
+ ],
177
+ games: [{ scores: [11, 8] }, { scores: [11, 5] }],
178
+ },
179
+ {
180
+ identifier: 'm2',
181
+ teams: [['vair_mem_eee'], ['vair_mem_fff']], // singles
182
+ games: [{ scores: [11, 9] }, { scores: [11, 7] }],
183
+ },
184
+ ],
98
185
  });
99
186
 
100
- const result = await client.submitMatch(match);
101
187
  if (result.ok) {
102
- console.log(`Submitted ${result.numGames} games`);
188
+ console.log(`Submitted ${result.numGames} games in ${result.numMatches} matches`);
103
189
  }
190
+ ```
104
191
 
105
- // Singles match
106
- const singles = new Match({
107
- event: 'Club Singles',
108
- bracket: 'Open Singles',
109
- date: new Date(),
110
- team1: ['player1_id'],
111
- team2: ['player2_id'],
112
- scores: [[11, 8], [9, 11], [11, 6]],
192
+ Set `dryRun: true` on the batch to validate without persisting — your API key must have
193
+ the `key:dry-run` scope.
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
+ ],
113
225
  });
114
- await client.submitMatch(singles);
226
+ console.log(`Imported ${result.matchesImported} matches, ${result.ghostPlayersCreated} ghosts`);
115
227
  ```
116
228
 
117
- ### Get Rating Updates
229
+ ## Webhook Deliveries
118
230
 
119
- ```typescript
120
- // First, look up members to subscribe to their updates
121
- await client.getMember('user_1');
122
- await client.getMember('user_2');
231
+ Inspect recent webhook delivery attempts for your app:
123
232
 
124
- // Later, check for rating changes
125
- const updates = await client.getRatingUpdates();
126
- for (const update of updates) {
127
- const direction = update.improved ? 'improved' : 'dropped';
128
- console.log(`${update.memberId} ${direction}: ${update.previousRating} -> ${update.newRating}`);
129
-
130
- // Get the full member profile
131
- const member = await update.getMember();
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);
132
237
  }
133
- ```
134
238
 
135
- ### OAuth Connect Flow
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
+ ```
136
243
 
137
- Connect players to your application using OAuth to access their profile and rating data.
244
+ ## OAuth Connect Flow
138
245
 
139
- ```typescript
140
- import { Vairified, generateState, OAuthError } from 'vairified';
246
+ ```ts
247
+ import { Vairified, OAuthError, generateState } from 'vairified';
141
248
 
142
- const client = new Vairified({ apiKey: 'vair_pk_xxx' });
249
+ await using client = new Vairified({ apiKey: 'vair_pk_xxx' });
143
250
 
144
- // Step 1: Start authorization
145
- const state = generateState(); // CSRF protection
146
- const auth = await client.startOAuth(
147
- 'https://myapp.com/oauth/callback',
148
- ['profile:read', 'rating:read', 'match:submit'],
251
+ // Step 1 start authorization
252
+ const state = generateState();
253
+ const auth = await client.oauth.authorize({
254
+ redirectUri: 'https://myapp.com/oauth/callback',
255
+ scopes: ['user:profile:read', 'user:rating:read', 'user:match:submit'],
149
256
  state,
150
- );
151
-
257
+ });
152
258
  // Redirect user to auth.authorizationUrl
153
- window.location.href = auth.authorizationUrl;
154
- ```
155
259
 
156
- ```typescript
157
- // Step 2: Handle callback (in your /oauth/callback route)
158
- const client = new Vairified({ apiKey: 'vair_pk_xxx' });
159
-
160
- // Exchange code for tokens
161
- const code = new URL(window.location.href).searchParams.get('code')!;
162
- const tokens = await client.exchangeToken(code, 'https://myapp.com/oauth/callback');
163
-
164
- // Store tokens securely
165
- const { playerId, accessToken, refreshToken } = tokens;
166
-
167
- // Now you can access the player's data
168
- const member = await client.getMember(playerId);
169
- console.log(`Connected: ${member.name} (${member.rating})`);
170
- ```
260
+ // Step 2 — exchange the callback code
261
+ const tokens = await client.oauth.exchangeToken({
262
+ code: 'code-from-callback',
263
+ redirectUri: 'https://myapp.com/oauth/callback',
264
+ });
265
+ const { accessToken, refreshToken, playerId } = tokens;
171
266
 
172
- ```typescript
173
- // Step 3: Refresh expired tokens
267
+ // Step 3 — refresh when the access token expires
174
268
  try {
175
- const newTokens = await client.refreshAccessToken(storedRefreshToken);
176
- // Update stored tokens
177
- } catch (e) {
178
- if (e instanceof OAuthError && e.errorCode === 'invalid_grant') {
179
- // Token revoked, user needs to re-authorize
269
+ const newTokens = await client.oauth.refresh(refreshToken!);
270
+ } catch (err) {
271
+ if (err instanceof OAuthError && err.errorCode === 'invalid_grant') {
272
+ // User must re-authorize
180
273
  }
181
274
  }
182
- ```
183
-
184
- ### Available OAuth Scopes
185
-
186
- | Scope | Description |
187
- |-------|-------------|
188
- | `profile:read` | Name, location, verification status |
189
- | `profile:email` | Email address |
190
- | `rating:read` | Current rating and rating splits |
191
- | `rating:history` | Complete rating history |
192
- | `match:submit` | Submit matches on behalf of user |
193
- | `webhook:subscribe` | Rating change notifications |
194
275
 
195
- ### Revoke Connection
196
-
197
- ```typescript
198
- await client.revokeConnection('vair_mem_xxx');
276
+ // Step 4 — revoke the connection
277
+ await client.oauth.revoke(playerId);
199
278
  ```
200
279
 
201
- ## Models
280
+ `OAuthScope` is a string literal union, so your editor will catch typos:
202
281
 
203
- ### Player
204
-
205
- ```typescript
206
- player.id // UUID or member ID
207
- player.memberId // Legacy member ID
208
- player.name // "John Smith"
209
- player.firstName // "John"
210
- player.lastName // "Smith"
211
- player.rating // 4.25
212
- player.isVairified // true/false
213
- player.ratingSplits // RatingSplits object
214
- player.city // "Austin"
215
- player.state // "TX"
216
- player.verifiedRating // Best verified rating
217
- ```
218
-
219
- ### Member (extends Player)
282
+ ```ts
283
+ import type { OAuthScope } from 'vairified';
220
284
 
221
- ```typescript
222
- member.email // Email address
223
- await member.refresh() // Refresh data from API
285
+ const scopes: OAuthScope[] = ['user:profile:read', 'user:rating:read']; // ok
286
+ const bad: OAuthScope[] = ['user:profile:read', 'rating']; // type error
224
287
  ```
225
288
 
226
- ### RatingSplits
289
+ ### Available scopes
227
290
 
228
- Access ratings by category:
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 |
229
299
 
230
- ```typescript
231
- const splits = member.ratingSplits;
232
- splits.open // Open division rating
233
- splits.gender // Same-gender doubles rating
234
- splits.mixed // Mixed doubles rating
235
- splits.recreational // Recreational rating
236
- splits.singles // Singles rating
237
- splits.best // Best available rating
238
- splits.get('50_and_up') // Age bracket rating
239
- ```
300
+ ## Leaderboards
240
301
 
241
- ### Match
302
+ ```ts
303
+ // Global leaderboard
304
+ const lb = await client.leaderboard.list();
242
305
 
243
- ```typescript
244
- const match = new Match({
245
- event: 'Weekly League',
246
- bracket: '4.0 Doubles',
247
- date: new Date(),
248
- team1: ['id1', 'id2'], // Player IDs for team 1
249
- team2: ['id3', 'id4'], // Player IDs for team 2
250
- scores: [[11, 9], [11, 7]], // Game scores
251
- location: 'Austin Club', // Optional
252
- matchType: 'SIDEOUT', // Default: "SIDEOUT"
253
- source: 'PARTNER', // Default: "PARTNER"
306
+ // Texas singles, verified only
307
+ const tx = await client.leaderboard.list({
308
+ category: 'singles',
309
+ scope: 'state',
310
+ state: 'TX',
311
+ verifiedOnly: true,
312
+ limit: 50,
254
313
  });
255
314
 
256
- match.format // "DOUBLES" or "SINGLES"
257
- match.winner // 1 or 2 (0 if tie)
258
- match.scoreSummary // "11-9, 11-7"
259
- match.identifier // Auto-generated unique ID
260
- ```
261
-
262
- ### MatchResult
263
-
264
- ```typescript
265
- const result = await client.submitMatches([match1, match2]);
266
- result.success // true/false
267
- result.numMatches // Number processed
268
- result.numGames // Games recorded
269
- result.dryRun // true if validation only
270
- result.message // Human-readable message
271
- result.errors // Array of errors
272
- result.ok // true if successful
273
- ```
315
+ // A specific player's rank with 5 players on either side
316
+ const rank = await client.leaderboard.rank('vair_mem_xxx', {
317
+ category: 'doubles',
318
+ contextSize: 5,
319
+ });
274
320
 
275
- ### SearchResults
276
-
277
- ```typescript
278
- results.players // Array of Player objects
279
- results.total // Total matching players
280
- results.page // Current page
281
- results.pages // Total pages
282
- results.hasMore // More pages available
283
- await results.nextPage() // Get next page
284
- results.length // Players on current page
285
- results.at(0) // Get by index
286
- for (const p of results) { } // Iterable
321
+ // Available categories, brackets, scopes
322
+ const categories = await client.leaderboard.categories();
287
323
  ```
288
324
 
289
325
  ## Configuration
290
326
 
291
- ```typescript
327
+ ```ts
292
328
  import { Vairified } from 'vairified';
293
329
 
294
- // Basic usage
295
- const client = new Vairified({ apiKey: 'vair_pk_xxx' });
330
+ // Environment preset
331
+ const client = new Vairified({ apiKey: 'vair_pk_xxx', env: 'production' }); // default
332
+ const staging = new Vairified({ apiKey: 'vair_pk_xxx', env: 'staging' });
333
+ const local = new Vairified({ apiKey: 'vair_pk_xxx', env: 'local' });
296
334
 
297
- // Use staging for development/testing
298
- const client = new Vairified({ apiKey: 'vair_pk_xxx', env: 'staging' });
335
+ // Custom base URL (overrides env)
336
+ const custom = new Vairified({
337
+ apiKey: 'vair_pk_xxx',
338
+ baseUrl: 'http://localhost:3001/api/v1',
339
+ timeoutMs: 30_000,
340
+ });
299
341
 
300
- // Custom configuration
301
- const client = new Vairified({
342
+ // Inject a custom fetch (test shims, non-Node environments)
343
+ const withFetch = new Vairified({
302
344
  apiKey: 'vair_pk_xxx',
303
- timeout: 30000,
345
+ fetch: customFetchImpl,
304
346
  });
305
347
  ```
306
348
 
307
- ### Environment Variables
349
+ ### Environment variables
308
350
 
309
351
  ```bash
310
352
  export VAIRIFIED_API_KEY="vair_pk_xxx"
353
+ export VAIRIFIED_ENV="staging" # optional; default: production
311
354
  ```
312
355
 
313
- ```typescript
314
- // API key read from environment (Node.js only)
315
- const client = new Vairified();
316
- ```
317
-
318
- ## Dry-Run Mode (Dev Keys)
319
-
320
- If your API key has the `dry-run` scope, match submissions are **validated but not persisted**. This is useful for testing integrations without affecting production data.
321
-
322
- ```typescript
323
- // With a dry-run API key
324
- const client = new Vairified({ apiKey: 'vair_pk_dev_xxx' });
325
- const result = await client.submitMatches([match1, match2]);
326
-
327
- if (result.dryRun) {
328
- console.log(`Validation passed: ${result.numGames} games would be created`);
329
- console.log(result.message);
330
- }
356
+ ```ts
357
+ const client = new Vairified(); // reads both env vars
331
358
  ```
332
359
 
333
- Request a dry-run API key from your Vairified partner contact for integration testing.
334
-
335
360
  ## Error Handling
336
361
 
337
- ```typescript
362
+ The SDK maps HTTP status codes to typed exceptions. All typed exceptions inherit from
363
+ `VairifiedError`, so a single `catch` can handle everything.
364
+
365
+ ```ts
338
366
  import {
339
367
  Vairified,
340
368
  VairifiedError,
341
369
  RateLimitError,
342
370
  AuthenticationError,
343
371
  NotFoundError,
372
+ ValidationError,
344
373
  OAuthError,
345
374
  } from 'vairified';
346
375
 
347
- const client = new Vairified({ apiKey: 'vair_pk_xxx' });
348
-
349
376
  try {
350
- const member = await client.getMember('user_123');
351
- } catch (error) {
352
- if (error instanceof RateLimitError) {
353
- console.log(`Rate limited. Retry after ${error.retryAfter} seconds`);
354
- } else if (error instanceof AuthenticationError) {
377
+ const member = await client.members.get('vair_mem_xxx');
378
+ } catch (err) {
379
+ if (err instanceof RateLimitError) {
380
+ console.log(`Rate limited; retry after ${err.retryAfter}s`);
381
+ } else if (err instanceof AuthenticationError) {
355
382
  console.log('Invalid API key');
356
- } else if (error instanceof NotFoundError) {
383
+ } else if (err instanceof NotFoundError) {
357
384
  console.log('Member not found');
358
- } else if (error instanceof OAuthError) {
359
- console.log(`OAuth error: ${error.message} (code: ${error.errorCode})`);
360
- } else if (error instanceof VairifiedError) {
361
- console.log(`API error: ${error.message} (status: ${error.statusCode})`);
385
+ } else if (err instanceof ValidationError) {
386
+ console.log(`Bad request: ${err.message}`);
387
+ } else if (err instanceof OAuthError) {
388
+ console.log(`OAuth error: ${err.message} (code: ${err.errorCode})`);
389
+ } else if (err instanceof VairifiedError) {
390
+ console.log(`API error: ${err.message} (status: ${err.statusCode})`);
391
+ } else {
392
+ throw err;
362
393
  }
363
394
  }
364
395
  ```
365
396
 
366
- ## TypeScript
397
+ ## Models
367
398
 
368
- Full TypeScript support with exported types:
399
+ Response models are immutable classes wrapping the wire payload. They expose computed
400
+ getters (`member.name`, `update.delta`) and support iteration where it makes sense
401
+ (`for (const [key, split] of sportRating)`).
402
+
403
+ ### `Member`
404
+
405
+ ```ts
406
+ member.memberId // Numeric member ID (public)
407
+ member.id // UUID | null
408
+ member.name // Full name (getter)
409
+ member.displayName // "Mike B."
410
+ member.firstName / lastName
411
+ member.gender // 'MALE' | 'FEMALE' | 'OTHER' | 'UNKNOWN' | null
412
+ member.age / city / state / zip / country
413
+ member.status.isVairified // grouped status flags
414
+ member.status.isConnected
415
+ member.sport // MemberSportMap
416
+ member.sports // readonly string[] of sport codes
417
+ member.ratingFor('pickleball') // number | null
418
+ member.split('overall-open') // RatingSplitWire | null
419
+ ```
369
420
 
370
- ```typescript
371
- import type {
372
- MatchInput,
373
- MatchResultData,
374
- PlayerData,
375
- SearchFilters,
376
- VairifiedOptions,
377
- } from 'vairified';
421
+ ### `SportRating` (dict-like)
422
+
423
+ ```ts
424
+ const pb = member.sport.get('pickleball');
425
+ pb?.rating // Primary rating for this sport
426
+ pb?.abbr // "VO", "VG", etc.
427
+ pb?.get('overall-open') // Any split key
428
+ pb?.size // Number of splits
429
+ pb?.has('singles-40+') // Membership check
430
+ for (const [key, split] of pb ?? []) { /* iterate */ }
378
431
  ```
379
432
 
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()`.
438
+
439
+ **From 0.1.x → 0.2.0:** Full rewrite. See the
440
+ [migration guide](https://vairified.github.io/vairified.js/documents/Migrating_from_0.1.x.html)
441
+ for the full diff and [CHANGELOG.md](CHANGELOG.md) for the release notes.
442
+
380
443
  ## Development
381
444
 
382
445
  ```bash
@@ -384,16 +447,17 @@ git clone https://github.com/Vairified/vairified.js.git
384
447
  cd vairified.js
385
448
  npm install
386
449
  npm test
450
+ npm run build
387
451
  ```
388
452
 
389
453
  ## License
390
454
 
391
- MIT License - see [LICENSE](LICENSE) for details.
455
+ MIT see [LICENSE](LICENSE) for details.
392
456
 
393
457
  ---
394
458
 
395
459
  <p align="center">
396
- <a href="https://vairified.com">vairified.com</a> ·
397
- <a href="https://vairified.github.io/vairified.js">Documentation</a> ·
460
+ <a href="https://vairified.com">vairified.com</a> ·
461
+ <a href="https://vairified.github.io/vairified.js">Documentation</a> ·
398
462
  <a href="mailto:support@vairified.com">Support</a>
399
463
  </p>