vairified 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,342 @@ 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`, `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 |
60
+
61
+ ## Members
62
+
63
+ ### Get a connected member
64
+
65
+ ```ts
66
+ const member = await client.members.get('vair_mem_xxx');
67
+
68
+ console.log(member.name); // Full name
69
+ console.log(member.displayName); // "Mike B."
70
+ console.log(member.ratingFor('pickleball')); // 3.915
71
+ console.log(member.status.isVairified); // true
72
+
73
+ // Dict-like access to rating splits for a specific sport
74
+ const pb = member.sport.get('pickleball');
75
+ if (pb) {
76
+ console.log(pb.rating, pb.abbr); // 3.915 VO
77
+ console.log(pb.get('overall-open')?.rating); // 3.915
78
+ console.log(pb.has('singles-open')); // true
79
+ for (const [key, split] of pb) {
80
+ console.log(key, split.rating);
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### Filter ratings to specific sports
86
+
87
+ ```ts
88
+ // Just pickleball
89
+ const member = await client.members.get('vair_mem_xxx', { sport: 'pickleball' });
90
+
91
+ // Multiple sports
92
+ const member2 = await client.members.get('vair_mem_xxx', {
93
+ sport: ['pickleball', 'padel'],
94
+ });
95
+ ```
96
+
97
+ ### Auto-paginating search
98
+
99
+ `search()` is an async generator — iterate directly with `for await`. Pages are fetched
100
+ lazily, so memory usage stays bounded regardless of result count.
101
+
102
+ ```ts
103
+ for await (const member of client.members.search({
54
104
  city: 'Austin',
55
105
  state: 'TX',
56
106
  ratingMin: 3.5,
57
107
  ratingMax: 4.5,
58
108
  vairifiedOnly: true,
59
- limit: 20,
60
- });
61
-
62
- // Iterate over results
63
- for (const player of results) {
64
- console.log(`${player.name}: ${player.rating}`);
109
+ })) {
110
+ console.log(member.name, member.ratingFor('pickleball'));
65
111
  }
66
112
 
67
- // Pagination
68
- console.log(`Page ${results.page} of ${results.pages}`);
69
- if (results.hasMore) {
70
- const nextPage = await results.nextPage();
113
+ // Cap with maxResults, or break out early
114
+ const top20: Member[] = [];
115
+ for await (const m of client.members.search({ name: 'Smith', maxResults: 20 })) {
116
+ top20.push(m);
71
117
  }
72
118
  ```
73
119
 
74
- ### Find a Player by Name
120
+ ### Find by name (first hit only)
75
121
 
76
- ```typescript
77
- const player = await client.findPlayer('John Smith');
78
- if (player) {
79
- console.log(`Found: ${player.name} (${player.rating})`);
122
+ ```ts
123
+ const mike = await client.members.find('Mike Barker');
124
+ if (mike) {
125
+ console.log(mike.ratingFor('pickleball'));
80
126
  }
81
127
  ```
82
128
 
83
- ### Submit Match Results
84
-
85
- ```typescript
86
- import { Vairified, Match } from 'vairified';
129
+ ### Rating change notifications
87
130
 
88
- const client = new Vairified({ apiKey: 'vair_pk_xxx' });
89
-
90
- // Doubles match: 11-9, 11-7
91
- const match = new Match({
92
- event: 'Weekly League',
93
- 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]],
98
- });
99
-
100
- const result = await client.submitMatch(match);
101
- if (result.ok) {
102
- console.log(`Submitted ${result.numGames} games`);
131
+ ```ts
132
+ const updates = await client.members.ratingUpdates();
133
+ for (const update of updates) {
134
+ const arrow = update.improved ? '↑' : '↓';
135
+ console.log(`${update.displayName} ${arrow} delta=${update.delta?.toFixed(3)}`);
103
136
  }
104
-
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]],
113
- });
114
- await client.submitMatch(singles);
115
137
  ```
116
138
 
117
- ### Get Rating Updates
139
+ ## Match Submission
118
140
 
119
- ```typescript
120
- // First, look up members to subscribe to their updates
121
- await client.getMember('user_1');
122
- await client.getMember('user_2');
141
+ Matches are submitted as a `MatchBatch` — defaults at the batch level apply to every
142
+ match unless overridden. The shape is n-team × n-game, so singles, doubles, and
143
+ round-robin all go through the same path.
123
144
 
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}`);
145
+ ```ts
146
+ const result = await client.matches.submit({
147
+ sport: 'pickleball',
148
+ winScore: 11,
149
+ winBy: 2,
150
+ bracket: '4.0 Doubles',
151
+ event: 'Weekly League',
152
+ matchDate: '2026-04-11T14:00:00Z',
153
+ matches: [
154
+ {
155
+ identifier: 'm1',
156
+ teams: [
157
+ ['vair_mem_aaa', 'vair_mem_bbb'],
158
+ ['vair_mem_ccc', 'vair_mem_ddd'],
159
+ ],
160
+ games: [{ scores: [11, 8] }, { scores: [11, 5] }],
161
+ },
162
+ {
163
+ identifier: 'm2',
164
+ teams: [['vair_mem_eee'], ['vair_mem_fff']], // singles
165
+ games: [{ scores: [11, 9] }, { scores: [11, 7] }],
166
+ },
167
+ ],
168
+ });
129
169
 
130
- // Get the full member profile
131
- const member = await update.getMember();
170
+ if (result.ok) {
171
+ console.log(`Submitted ${result.numGames} games in ${result.numMatches} matches`);
132
172
  }
133
173
  ```
134
174
 
135
- ### OAuth Connect Flow
175
+ Set `dryRun: true` on the batch to validate without persisting — your API key must have
176
+ the `dry-run` scope.
136
177
 
137
- Connect players to your application using OAuth to access their profile and rating data.
178
+ ## OAuth Connect Flow
138
179
 
139
- ```typescript
140
- import { Vairified, generateState, OAuthError } from 'vairified';
180
+ ```ts
181
+ import { Vairified, OAuthError, generateState } from 'vairified';
141
182
 
142
- const client = new Vairified({ apiKey: 'vair_pk_xxx' });
183
+ await using client = new Vairified({ apiKey: 'vair_pk_xxx' });
143
184
 
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'],
185
+ // Step 1 start authorization
186
+ const state = generateState();
187
+ const auth = await client.oauth.authorize({
188
+ redirectUri: 'https://myapp.com/oauth/callback',
189
+ scopes: ['profile:read', 'rating:read', 'match:submit'],
149
190
  state,
150
- );
151
-
191
+ });
152
192
  // Redirect user to auth.authorizationUrl
153
- window.location.href = auth.authorizationUrl;
154
- ```
155
-
156
- ```typescript
157
- // Step 2: Handle callback (in your /oauth/callback route)
158
- const client = new Vairified({ apiKey: 'vair_pk_xxx' });
159
193
 
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
- ```
194
+ // Step 2 exchange the callback code
195
+ const tokens = await client.oauth.exchangeToken({
196
+ code: 'code-from-callback',
197
+ redirectUri: 'https://myapp.com/oauth/callback',
198
+ });
199
+ const { accessToken, refreshToken, playerId } = tokens;
171
200
 
172
- ```typescript
173
- // Step 3: Refresh expired tokens
201
+ // Step 3 — refresh when the access token expires
174
202
  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
203
+ const newTokens = await client.oauth.refresh(refreshToken!);
204
+ } catch (err) {
205
+ if (err instanceof OAuthError && err.errorCode === 'invalid_grant') {
206
+ // User must re-authorize
180
207
  }
181
208
  }
182
- ```
183
209
 
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
-
195
- ### Revoke Connection
196
-
197
- ```typescript
198
- await client.revokeConnection('vair_mem_xxx');
210
+ // Step 4 — revoke the connection
211
+ await client.oauth.revoke(playerId);
199
212
  ```
200
213
 
201
- ## Models
202
-
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
- ```
214
+ `OAuthScope` is a string literal union, so your editor will catch typos:
218
215
 
219
- ### Member (extends Player)
216
+ ```ts
217
+ import type { OAuthScope } from 'vairified';
220
218
 
221
- ```typescript
222
- member.email // Email address
223
- await member.refresh() // Refresh data from API
219
+ const scopes: OAuthScope[] = ['profile:read', 'rating:read']; // ok
220
+ const bad: OAuthScope[] = ['profile:read', 'rating']; // type error
224
221
  ```
225
222
 
226
- ### RatingSplits
223
+ ### Available scopes
227
224
 
228
- Access ratings by category:
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 |
229
233
 
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
- ```
234
+ ## Leaderboards
240
235
 
241
- ### Match
236
+ ```ts
237
+ // Global leaderboard
238
+ const lb = await client.leaderboard.list();
242
239
 
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"
240
+ // Texas singles, verified only
241
+ const tx = await client.leaderboard.list({
242
+ category: 'singles',
243
+ scope: 'state',
244
+ state: 'TX',
245
+ verifiedOnly: true,
246
+ limit: 50,
254
247
  });
255
248
 
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
- ```
249
+ // A specific player's rank with 5 players on either side
250
+ const rank = await client.leaderboard.rank('vair_mem_xxx', {
251
+ category: 'doubles',
252
+ contextSize: 5,
253
+ });
274
254
 
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
255
+ // Available categories, brackets, scopes
256
+ const categories = await client.leaderboard.categories();
287
257
  ```
288
258
 
289
259
  ## Configuration
290
260
 
291
- ```typescript
261
+ ```ts
292
262
  import { Vairified } from 'vairified';
293
263
 
294
- // Basic usage
295
- const client = new Vairified({ apiKey: 'vair_pk_xxx' });
264
+ // Environment preset
265
+ const client = new Vairified({ apiKey: 'vair_pk_xxx', env: 'production' }); // default
266
+ const staging = new Vairified({ apiKey: 'vair_pk_xxx', env: 'staging' });
267
+ const local = new Vairified({ apiKey: 'vair_pk_xxx', env: 'local' });
296
268
 
297
- // Use staging for development/testing
298
- const client = new Vairified({ apiKey: 'vair_pk_xxx', env: 'staging' });
269
+ // Custom base URL (overrides env)
270
+ const custom = new Vairified({
271
+ apiKey: 'vair_pk_xxx',
272
+ baseUrl: 'http://localhost:3001/api/v1',
273
+ timeoutMs: 30_000,
274
+ });
299
275
 
300
- // Custom configuration
301
- const client = new Vairified({
276
+ // Inject a custom fetch (test shims, non-Node environments)
277
+ const withFetch = new Vairified({
302
278
  apiKey: 'vair_pk_xxx',
303
- timeout: 30000,
279
+ fetch: customFetchImpl,
304
280
  });
305
281
  ```
306
282
 
307
- ### Environment Variables
283
+ ### Environment variables
308
284
 
309
285
  ```bash
310
286
  export VAIRIFIED_API_KEY="vair_pk_xxx"
287
+ export VAIRIFIED_ENV="staging" # optional; default: production
311
288
  ```
312
289
 
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
- }
290
+ ```ts
291
+ const client = new Vairified(); // reads both env vars
331
292
  ```
332
293
 
333
- Request a dry-run API key from your Vairified partner contact for integration testing.
334
-
335
294
  ## Error Handling
336
295
 
337
- ```typescript
296
+ The SDK maps HTTP status codes to typed exceptions. All typed exceptions inherit from
297
+ `VairifiedError`, so a single `catch` can handle everything.
298
+
299
+ ```ts
338
300
  import {
339
301
  Vairified,
340
302
  VairifiedError,
341
303
  RateLimitError,
342
304
  AuthenticationError,
343
305
  NotFoundError,
306
+ ValidationError,
344
307
  OAuthError,
345
308
  } from 'vairified';
346
309
 
347
- const client = new Vairified({ apiKey: 'vair_pk_xxx' });
348
-
349
310
  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) {
311
+ const member = await client.members.get('vair_mem_xxx');
312
+ } catch (err) {
313
+ if (err instanceof RateLimitError) {
314
+ console.log(`Rate limited; retry after ${err.retryAfter}s`);
315
+ } else if (err instanceof AuthenticationError) {
355
316
  console.log('Invalid API key');
356
- } else if (error instanceof NotFoundError) {
317
+ } else if (err instanceof NotFoundError) {
357
318
  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})`);
319
+ } else if (err instanceof ValidationError) {
320
+ console.log(`Bad request: ${err.message}`);
321
+ } else if (err instanceof OAuthError) {
322
+ console.log(`OAuth error: ${err.message} (code: ${err.errorCode})`);
323
+ } else if (err instanceof VairifiedError) {
324
+ console.log(`API error: ${err.message} (status: ${err.statusCode})`);
325
+ } else {
326
+ throw err;
362
327
  }
363
328
  }
364
329
  ```
365
330
 
366
- ## TypeScript
331
+ ## Models
367
332
 
368
- Full TypeScript support with exported types:
333
+ Response models are immutable classes wrapping the wire payload. They expose computed
334
+ getters (`member.name`, `update.delta`) and support iteration where it makes sense
335
+ (`for (const [key, split] of sportRating)`).
336
+
337
+ ### `Member`
338
+
339
+ ```ts
340
+ member.memberId // Numeric member ID (public)
341
+ member.id // UUID | null
342
+ member.name // Full name (getter)
343
+ member.displayName // "Mike B."
344
+ member.firstName / lastName
345
+ member.gender // 'MALE' | 'FEMALE' | 'OTHER' | 'UNKNOWN' | null
346
+ member.age / city / state / zip / country
347
+ member.status.isVairified // grouped status flags
348
+ member.status.isConnected
349
+ member.sport // MemberSportMap
350
+ member.sports // readonly string[] of sport codes
351
+ member.ratingFor('pickleball') // number | null
352
+ member.split('overall-open') // RatingSplitWire | null
353
+ ```
369
354
 
370
- ```typescript
371
- import type {
372
- MatchInput,
373
- MatchResultData,
374
- PlayerData,
375
- SearchFilters,
376
- VairifiedOptions,
377
- } from 'vairified';
355
+ ### `SportRating` (dict-like)
356
+
357
+ ```ts
358
+ const pb = member.sport.get('pickleball');
359
+ pb?.rating // Primary rating for this sport
360
+ pb?.abbr // "VO", "VG", etc.
361
+ pb?.get('overall-open') // Any split key
362
+ pb?.size // Number of splits
363
+ pb?.has('singles-40+') // Membership check
364
+ for (const [key, split] of pb ?? []) { /* iterate */ }
378
365
  ```
379
366
 
367
+ ## Migrating from 0.1.x
368
+
369
+ Version 0.2.0 is a breaking rewrite. See the
370
+ [migration guide](https://vairified.github.io/vairified.js/documents/Migrating_from_0.1.x.html)
371
+ for the full diff and [CHANGELOG.md](CHANGELOG.md) for the release notes.
372
+
380
373
  ## Development
381
374
 
382
375
  ```bash
@@ -384,16 +377,17 @@ git clone https://github.com/Vairified/vairified.js.git
384
377
  cd vairified.js
385
378
  npm install
386
379
  npm test
380
+ npm run build
387
381
  ```
388
382
 
389
383
  ## License
390
384
 
391
- MIT License - see [LICENSE](LICENSE) for details.
385
+ MIT see [LICENSE](LICENSE) for details.
392
386
 
393
387
  ---
394
388
 
395
389
  <p align="center">
396
- <a href="https://vairified.com">vairified.com</a> ·
397
- <a href="https://vairified.github.io/vairified.js">Documentation</a> ·
390
+ <a href="https://vairified.com">vairified.com</a> ·
391
+ <a href="https://vairified.github.io/vairified.js">Documentation</a> ·
398
392
  <a href="mailto:support@vairified.com">Support</a>
399
393
  </p>