vairified 0.4.0 → 0.7.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/dist/index.d.ts CHANGED
@@ -138,6 +138,19 @@ type VairProStatus = 'PENDING' | 'ACTIVE' | null;
138
138
  interface MemberStatusWire {
139
139
  readonly isWheelchair: boolean;
140
140
  readonly isAmbassador: boolean;
141
+ /**
142
+ * Whether the player currently holds a **paid VAIR+ membership** — the field to check when a
143
+ * partner requires VAIR+ for entry.
144
+ *
145
+ * `false` covers both "never bought" and "bought once, no longer active", and is also `false`
146
+ * during the automatic 30-day trial every new VAIR account receives: a trial is not a paid
147
+ * membership.
148
+ *
149
+ * Not to be confused with the per-sport `isVairPro` / `isRater` on each
150
+ * {@link SportRatingWire} — that is the VAIR **Pro** certified-rater programme, a different
151
+ * product whose name differs by two characters.
152
+ */
153
+ readonly isVairPlus: boolean;
141
154
  readonly isConnected: boolean;
142
155
  }
143
156
  /**
@@ -157,6 +170,7 @@ interface PartnerMemberWire {
157
170
  readonly state?: string;
158
171
  readonly zip?: string;
159
172
  readonly country?: string;
173
+ readonly memberSince?: string;
160
174
  readonly gender?: Gender;
161
175
  readonly status: MemberStatusWire;
162
176
  readonly sport?: Readonly<Record<string, SportRatingWire>>;
@@ -164,6 +178,30 @@ interface PartnerMemberWire {
164
178
  readonly email?: string;
165
179
  readonly grantedScopes?: readonly string[];
166
180
  }
181
+ /**
182
+ * One resolved address in a {@link MembersByEmailResultWire}.
183
+ *
184
+ * `members` is always an array: an email address is not a unique key in
185
+ * VAIR, so one address can resolve to more than one member.
186
+ *
187
+ * @category Members
188
+ */
189
+ interface PartnerMemberEmailMatchWire {
190
+ readonly email: string;
191
+ readonly members: readonly PartnerMemberWire[];
192
+ }
193
+ /**
194
+ * Raw envelope returned by `GET /partner/members/by-email`.
195
+ *
196
+ * Every requested address appears in exactly one of `matched` or
197
+ * `notFound` — the endpoint never silently drops one.
198
+ *
199
+ * @category Members
200
+ */
201
+ interface MembersByEmailResultWire {
202
+ readonly matched: readonly PartnerMemberEmailMatchWire[];
203
+ readonly notFound: readonly string[];
204
+ }
167
205
  /**
168
206
  * Raw rating change notification.
169
207
  *
@@ -356,6 +394,10 @@ interface PlayerRankOptions {
356
394
  *
357
395
  * @category Matches
358
396
  */
397
+ interface TournamentImportCreatedGhostWire {
398
+ readonly ref: string;
399
+ readonly memberId: number;
400
+ }
359
401
  interface TournamentImportResultWire {
360
402
  readonly success: boolean;
361
403
  readonly matchesImported: number;
@@ -365,6 +407,7 @@ interface TournamentImportResultWire {
365
407
  readonly dryRun?: boolean;
366
408
  readonly message?: string;
367
409
  readonly errors?: readonly string[];
410
+ readonly createdGhostMembers?: readonly TournamentImportCreatedGhostWire[];
368
411
  }
369
412
  /**
370
413
  * Wire shape for a single webhook delivery attempt.
@@ -405,6 +448,24 @@ interface ApiErrorResponse {
405
448
  readonly error?: string;
406
449
  readonly statusCode?: number;
407
450
  }
451
+ type AttributionOutcome = 'attributed' | 'already_attributed' | 'account_predates_event' | 'not_found';
452
+ interface MemberAttributionWire {
453
+ readonly memberId: number;
454
+ readonly attributed: boolean;
455
+ readonly ambassadorMemberId?: number | null;
456
+ readonly attributedAt?: string;
457
+ }
458
+ interface MembersAttributionResultWire {
459
+ readonly attributions?: readonly MemberAttributionWire[];
460
+ readonly notFound?: readonly number[];
461
+ }
462
+ interface AttributionResultWire {
463
+ readonly attributed: number;
464
+ readonly results?: readonly {
465
+ readonly memberId: number;
466
+ readonly outcome: AttributionOutcome;
467
+ }[];
468
+ }
408
469
 
409
470
  /**
410
471
  * {@link LeaderboardResource} — read-only leaderboard queries.
@@ -464,6 +525,17 @@ declare class MatchBatchResult {
464
525
  * @module
465
526
  */
466
527
 
528
+ /**
529
+ * One ghost player created by a tournament import.
530
+ *
531
+ * @category Matches
532
+ */
533
+ interface TournamentImportCreatedGhost {
534
+ /** The email or phone you supplied for this person in `ghostMembers[]`. */
535
+ readonly ref: string;
536
+ /** The public member id allocated to them, usable in `matches.submit()`. */
537
+ readonly memberId: number;
538
+ }
467
539
  /**
468
540
  * Result of a tournament import submission.
469
541
  *
@@ -478,6 +550,19 @@ declare class TournamentImportResult {
478
550
  readonly dryRun: boolean;
479
551
  readonly message: string | undefined;
480
552
  readonly errors: readonly string[];
553
+ /**
554
+ * Public member ids for the ghost players THIS import created, keyed by the
555
+ * `ref` supplied in `ghostMembers[]`.
556
+ *
557
+ * Empty on a dry-run, and empty for entries the import matched to a player who
558
+ * already existed — resolving an existing email to a member requires the
559
+ * `key:player:lookup` scope and `members.getByEmail()`.
560
+ *
561
+ * Use these ids directly in a follow-up `matches.submit()`; without them an
562
+ * import reports only how many accounts it caused and you cannot address any
563
+ * of them.
564
+ */
565
+ readonly createdGhostMembers: readonly TournamentImportCreatedGhost[];
481
566
  /** @internal */
482
567
  constructor(wire: TournamentImportResultWire);
483
568
  /** True when the import succeeded without errors. */
@@ -683,6 +768,20 @@ declare class Member {
683
768
  readonly state: string | null;
684
769
  readonly zip: string | null;
685
770
  readonly country: string | null;
771
+ /**
772
+ * The DATE this member's VAIR account was created (`YYYY-MM-DD`, UTC), or
773
+ * `null` when the endpoint does not supply it.
774
+ *
775
+ * Present on `members.get()`, `members.getBulk()` and `members.getByEmail()` —
776
+ * the calls where you already know which member you asked about. **Never on
777
+ * `members.search()`**, which is discovery: account age is not something you
778
+ * can browse strangers by.
779
+ *
780
+ * Deliberately a date, not a timestamp. It exists so you can apply a
781
+ * new-accounts-only referral rule — crediting an ambassador only for accounts
782
+ * created because of their event.
783
+ */
784
+ readonly memberSince: string | null;
686
785
  readonly gender: Gender | null;
687
786
  readonly status: MemberStatusWire;
688
787
  readonly sport: MemberSportMap;
@@ -713,6 +812,74 @@ declare class Member {
713
812
  toString(): string;
714
813
  }
715
814
 
815
+ /**
816
+ * {@link MembersByEmailResult} — result of an email → member lookup.
817
+ *
818
+ * @module
819
+ */
820
+
821
+ /**
822
+ * One requested address that resolved to at least one member.
823
+ *
824
+ * {@link members} is always an array. An email address is not a unique
825
+ * key in VAIR — an unclaimed imported record can share an address with a
826
+ * claimed account — so never assume a single element without checking.
827
+ *
828
+ * @category Members
829
+ */
830
+ declare class MemberEmailMatch {
831
+ /** The address exactly as you supplied it, not as stored. */
832
+ readonly email: string;
833
+ /** Every member holding this address. Never empty. */
834
+ readonly members: readonly Member[];
835
+ /** @internal */
836
+ constructor(wire: PartnerMemberEmailMatchWire);
837
+ /**
838
+ * The single member for this address, or `null` when the address is
839
+ * ambiguous (more than one match).
840
+ *
841
+ * Use this rather than `members[0]` when a wrong link is worse than no
842
+ * link — it refuses to guess instead of silently picking one.
843
+ */
844
+ get sole(): Member | null;
845
+ /** Whether this address resolved to more than one member. */
846
+ get isAmbiguous(): boolean;
847
+ toString(): string;
848
+ }
849
+ /**
850
+ * Result of a {@link MembersResource.getByEmail} call.
851
+ *
852
+ * Every address you supplied appears in exactly one of {@link matched} or
853
+ * {@link notFound} — the endpoint never silently drops one, so consume
854
+ * {@link notFound} directly rather than diffing your input against the
855
+ * results.
856
+ *
857
+ * A `notFound` address is not proof the person has no VAIR account:
858
+ * unclaimed imported records are deliberately excluded from this lookup.
859
+ *
860
+ * @category Members
861
+ */
862
+ declare class MembersByEmailResult {
863
+ readonly matched: readonly MemberEmailMatch[];
864
+ /** Addresses that resolved to nothing, echoed as you supplied them. */
865
+ readonly notFound: readonly string[];
866
+ /** @internal */
867
+ constructor(wire: MembersByEmailResultWire);
868
+ /**
869
+ * Look up one address's match, case-insensitively.
870
+ *
871
+ * Saves callers a linear scan and, more importantly, saves them from
872
+ * matching case-sensitively against an address the server echoed back
873
+ * in whatever case they originally sent.
874
+ */
875
+ get(email: string): MemberEmailMatch | null;
876
+ /** Whether every requested address resolved to at least one member. */
877
+ get allResolved(): boolean;
878
+ /** Total number of members across every matched address. */
879
+ get memberCount(): number;
880
+ toString(): string;
881
+ }
882
+
716
883
  /**
717
884
  * {@link RatingUpdate} — a single rating change notification.
718
885
  *
@@ -850,6 +1017,50 @@ declare class MembersResource {
850
1017
  getBulk(ids: number[], options?: {
851
1018
  sport?: string;
852
1019
  }): Promise<Member[]>;
1020
+ /**
1021
+ * Resolve up to 100 members by their **exact** email address.
1022
+ *
1023
+ * Use this to link your users to their VAIR identity when you hold
1024
+ * their email but not their member ID — e.g. resolving a tournament
1025
+ * roster at registration instead of waiting for each player to
1026
+ * complete SSO.
1027
+ *
1028
+ * **Requires the `key:player:lookup` scope**, which is granted per
1029
+ * partner on approval. Holding `key:player:search` does not imply it.
1030
+ *
1031
+ * Matching is exact and case-insensitive; there is deliberately no
1032
+ * partial, prefix or fuzzy matching. Every address you supply comes
1033
+ * back in either `matched` or `notFound`, so read `notFound` directly
1034
+ * instead of diffing your input against the results.
1035
+ *
1036
+ * A `notFound` address is **not** proof the person has no VAIR
1037
+ * account — unclaimed imported records are excluded from this lookup.
1038
+ *
1039
+ * @param emails - Email addresses to resolve (max 100).
1040
+ * @param options - Optional filters.
1041
+ * @param options.sport - Sport code to scope ratings (e.g. `'pickleball'`).
1042
+ * @throws {@link ValidationError} If more than 100 addresses are
1043
+ * provided, or the list is empty.
1044
+ * @category Members
1045
+ *
1046
+ * @example
1047
+ * ```ts
1048
+ * const result = await client.members.getByEmail([
1049
+ * 'ada@example.com',
1050
+ * 'nobody@example.com',
1051
+ * ]);
1052
+ *
1053
+ * for (const match of result.matched) {
1054
+ * const member = match.sole; // null when the address is ambiguous
1055
+ * if (member) console.log(match.email, '->', member.memberId);
1056
+ * }
1057
+ *
1058
+ * console.log('no VAIR account found for:', result.notFound);
1059
+ * ```
1060
+ */
1061
+ getByEmail(emails: readonly string[], options?: {
1062
+ sport?: string;
1063
+ }): Promise<MembersByEmailResult>;
853
1064
  /**
854
1065
  * Poll for rating change notifications.
855
1066
  *
@@ -1050,6 +1261,170 @@ declare class OAuthResource {
1050
1261
  }[]>;
1051
1262
  }
1052
1263
 
1264
+ /**
1265
+ * {@link MemberAttribution}, {@link MembersAttributionResult} and
1266
+ * {@link AttributionResult} — ambassador referral credit.
1267
+ *
1268
+ * @module
1269
+ */
1270
+
1271
+ /**
1272
+ * Who currently earns referral credit for one member.
1273
+ *
1274
+ * @category Referrals
1275
+ */
1276
+ declare class MemberAttribution {
1277
+ readonly memberId: number;
1278
+ /** True when some ambassador already holds credit for this member. */
1279
+ readonly attributed: boolean;
1280
+ /**
1281
+ * The member id of the ambassador holding the credit.
1282
+ *
1283
+ * Compare it against your own event host's member id to tell "already credited
1284
+ * to my host — nothing to do" from "credited to somebody else — a person needs
1285
+ * to look, because claiming it takes credit from them".
1286
+ *
1287
+ * `null` both when nobody holds credit and when credit is held by a record that
1288
+ * has no member id of its own, so check {@link attributed} to tell those apart.
1289
+ */
1290
+ readonly ambassadorMemberId: number | null;
1291
+ /** The date the credit was established (`YYYY-MM-DD`), or `null`. */
1292
+ readonly attributedAt: string | null;
1293
+ /** @internal */
1294
+ constructor(wire: MemberAttributionWire);
1295
+ /** True when nobody holds credit yet, so this member can be claimed. */
1296
+ get isClaimable(): boolean;
1297
+ /** True when credit is held by an ambassador OTHER than the one given. */
1298
+ heldBySomeoneOtherThan(ambassadorMemberId: number): boolean;
1299
+ }
1300
+ /**
1301
+ * Attribution for a batch of members, plus the ids that matched nothing.
1302
+ *
1303
+ * @category Referrals
1304
+ */
1305
+ declare class MembersAttributionResult {
1306
+ readonly attributions: readonly MemberAttribution[];
1307
+ /**
1308
+ * Member ids that matched no member. Read this rather than diffing your input
1309
+ * against the results — every id you sent lands in one bucket or the other.
1310
+ */
1311
+ readonly notFound: readonly number[];
1312
+ /** @internal */
1313
+ constructor(wire: MembersAttributionResultWire);
1314
+ /** Attribution for one member id, or `undefined` if it was not returned. */
1315
+ get(memberId: number): MemberAttribution | undefined;
1316
+ /** Members nobody holds credit for yet. */
1317
+ get claimable(): readonly MemberAttribution[];
1318
+ }
1319
+ /**
1320
+ * Outcome of submitting attribution for a batch of members.
1321
+ *
1322
+ * @category Referrals
1323
+ */
1324
+ declare class AttributionResult {
1325
+ /** How many members were newly attributed by this request. */
1326
+ readonly attributed: number;
1327
+ /** One entry per member id supplied, in the order supplied. */
1328
+ readonly results: readonly Readonly<{
1329
+ memberId: number;
1330
+ outcome: AttributionOutcome;
1331
+ }>[];
1332
+ /** @internal */
1333
+ constructor(wire: AttributionResultWire);
1334
+ /** Member ids with the given outcome. */
1335
+ withOutcome(outcome: AttributionOutcome): readonly number[];
1336
+ /**
1337
+ * Members already credited to somebody. These are the ones worth a human
1338
+ * look — it may be your own host, or it may be another ambassador.
1339
+ */
1340
+ get alreadyAttributed(): readonly number[];
1341
+ /**
1342
+ * Members rejected because their account pre-dates the event's registration
1343
+ * page. The event did not recruit them, so no credit is due.
1344
+ */
1345
+ get predatedEvent(): readonly number[];
1346
+ }
1347
+
1348
+ /**
1349
+ * {@link ReferralsResource} — read and record ambassador referral credit.
1350
+ *
1351
+ * @module
1352
+ */
1353
+
1354
+ /**
1355
+ * Read and record which ambassador earns referral credit for a player.
1356
+ *
1357
+ * Each method needs its own API-key permission, granted per partner:
1358
+ * `key:referral:read` for {@link get} and `key:referral:write` for
1359
+ * {@link attribute}. Neither is implied by a general read, write or admin key —
1360
+ * reading attribution exposes who recruited whom, and writing it decides who
1361
+ * earns commission.
1362
+ *
1363
+ * @category Resources
1364
+ */
1365
+ declare class ReferralsResource {
1366
+ #private;
1367
+ /** @internal */
1368
+ constructor(http: HttpTransport);
1369
+ /**
1370
+ * Who currently earns referral credit for these members.
1371
+ *
1372
+ * Use this before {@link attribute} to tell the two cases apart that matter:
1373
+ * a member already credited to your own event host (nothing to do) and one
1374
+ * credited to a different ambassador (a person should look, because claiming
1375
+ * it takes credit from them).
1376
+ *
1377
+ * @example
1378
+ * ```ts
1379
+ * const result = await client.referrals.get([4873327, 4873328]);
1380
+ *
1381
+ * for (const a of result.claimable) {
1382
+ * console.log(a.memberId, 'has no credit yet');
1383
+ * }
1384
+ * console.log('no such member:', result.notFound);
1385
+ * ```
1386
+ *
1387
+ * @throws {@link ValidationError} If the list is empty or exceeds 100 ids.
1388
+ */
1389
+ get(memberIds: readonly number[]): Promise<MembersAttributionResult>;
1390
+ /**
1391
+ * Credit an ambassador for players their event recruited.
1392
+ *
1393
+ * `registrationPublishedAt` is the date the event's registration page was
1394
+ * **first published**. Accounts created before it did not come from the event
1395
+ * and are rejected with `account_predates_event`. VAIR applies that rule
1396
+ * itself, so every partner is held to the same one.
1397
+ *
1398
+ * VAIR cannot verify the date — it holds no record of your registration pages —
1399
+ * so the value you send is recorded for audit. Send the real one.
1400
+ *
1401
+ * Safe to retry: attribution is one-per-player forever, enforced by the
1402
+ * database, so a resubmitted player returns `already_attributed` and nothing
1403
+ * changes.
1404
+ *
1405
+ * @example
1406
+ * ```ts
1407
+ * const result = await client.referrals.attribute({
1408
+ * referralCode: 'hillhurst-open',
1409
+ * registrationPublishedAt: '2026-08-01',
1410
+ * memberIds: [4873327, 4873328],
1411
+ * });
1412
+ *
1413
+ * console.log(result.attributed, 'newly credited');
1414
+ * console.log('need a human:', result.alreadyAttributed);
1415
+ * console.log('too old to credit:', result.predatedEvent);
1416
+ * ```
1417
+ *
1418
+ * @throws {@link ValidationError} If the list is empty or exceeds 500 ids, or
1419
+ * the date is not `YYYY-MM-DD`.
1420
+ */
1421
+ attribute(input: {
1422
+ referralCode: string;
1423
+ registrationPublishedAt: string;
1424
+ memberIds: readonly number[];
1425
+ }): Promise<AttributionResult>;
1426
+ }
1427
+
1053
1428
  /**
1054
1429
  * {@link WebhookDelivery} and {@link WebhookDeliveriesResult} — webhook
1055
1430
  * delivery inspection models.
@@ -1202,6 +1577,11 @@ declare class Vairified {
1202
1577
  readonly leaderboard: LeaderboardResource;
1203
1578
  /** Webhook delivery inspection — deliveries. */
1204
1579
  readonly webhooks: WebhooksResource;
1580
+ /**
1581
+ * Read and record ambassador referral credit. Each method needs its own
1582
+ * per-partner permission — see {@link ReferralsResource}.
1583
+ */
1584
+ readonly referrals: ReferralsResource;
1205
1585
  constructor(options?: VairifiedOptions);
1206
1586
  /**
1207
1587
  * API usage statistics for the current API key.
@@ -1300,4 +1680,4 @@ declare class OAuthError extends VairifiedError {
1300
1680
  constructor(message?: string, errorCode?: string, response?: unknown);
1301
1681
  }
1302
1682
 
1303
- export { type ApiErrorResponse, AuthenticationError, type AuthorizationResponse, DEFAULT_SCOPES, ENVIRONMENTS, type GameInput, type Gender, type LeaderboardOptions, LeaderboardResource, type MatchBatch, MatchBatchResult, type MatchBatchResultWire, type MatchInput, MatchesResource, Member, MemberSportMap, type MemberStatusWire, MembersResource, NotFoundError, type OAuthConfig, OAuthError, OAuthResource, type OAuthScope, type PartnerMemberWire, type PartnerRatingUpdateWire, type PlayerRankOptions, RateLimitError, type RatingSplitWire, RatingUpdate, SCOPES, type SearchFilters, SportRating, type SportRatingWire, type TokenResponse, TournamentImportResult, type TournamentImportResultWire, Vairified, type VairifiedEnvironment, VairifiedError, type VairifiedOptions, ValidationError, WebhookDeliveriesResult, type WebhookDeliveriesResultWire, WebhookDelivery, type WebhookDeliveryWire, WebhooksResource, describeScope, describeScopes, generateState, getAuthorizationUrl, validateScope };
1683
+ export { type ApiErrorResponse, AuthenticationError, type AuthorizationResponse, DEFAULT_SCOPES, ENVIRONMENTS, type GameInput, type Gender, type LeaderboardOptions, LeaderboardResource, type MatchBatch, MatchBatchResult, type MatchBatchResultWire, type MatchInput, MatchesResource, Member, MemberEmailMatch, MemberSportMap, type MemberStatusWire, MembersByEmailResult, type MembersByEmailResultWire, MembersResource, NotFoundError, type OAuthConfig, OAuthError, OAuthResource, type OAuthScope, type PartnerMemberEmailMatchWire, type PartnerMemberWire, type PartnerRatingUpdateWire, type PlayerRankOptions, RateLimitError, type RatingSplitWire, RatingUpdate, SCOPES, type SearchFilters, SportRating, type SportRatingWire, type TokenResponse, TournamentImportResult, type TournamentImportResultWire, Vairified, type VairifiedEnvironment, VairifiedError, type VairifiedOptions, ValidationError, WebhookDeliveriesResult, type WebhookDeliveriesResultWire, WebhookDelivery, type WebhookDeliveryWire, WebhooksResource, describeScope, describeScopes, generateState, getAuthorizationUrl, validateScope };