vairified 0.3.2 → 0.6.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 +104 -3
- package/dist/index.cjs +419 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +401 -6
- package/dist/index.d.ts +401 -6
- package/dist/index.js +417 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -27,7 +27,9 @@ __export(index_exports, {
|
|
|
27
27
|
MatchBatchResult: () => MatchBatchResult,
|
|
28
28
|
MatchesResource: () => MatchesResource,
|
|
29
29
|
Member: () => Member,
|
|
30
|
+
MemberEmailMatch: () => MemberEmailMatch,
|
|
30
31
|
MemberSportMap: () => MemberSportMap,
|
|
32
|
+
MembersByEmailResult: () => MembersByEmailResult,
|
|
31
33
|
MembersResource: () => MembersResource,
|
|
32
34
|
NotFoundError: () => NotFoundError,
|
|
33
35
|
OAuthError: () => OAuthError,
|
|
@@ -307,6 +309,19 @@ var TournamentImportResult = class {
|
|
|
307
309
|
dryRun;
|
|
308
310
|
message;
|
|
309
311
|
errors;
|
|
312
|
+
/**
|
|
313
|
+
* Public member ids for the ghost players THIS import created, keyed by the
|
|
314
|
+
* `ref` supplied in `ghostMembers[]`.
|
|
315
|
+
*
|
|
316
|
+
* Empty on a dry-run, and empty for entries the import matched to a player who
|
|
317
|
+
* already existed — resolving an existing email to a member requires the
|
|
318
|
+
* `key:player:lookup` scope and `members.getByEmail()`.
|
|
319
|
+
*
|
|
320
|
+
* Use these ids directly in a follow-up `matches.submit()`; without them an
|
|
321
|
+
* import reports only how many accounts it caused and you cannot address any
|
|
322
|
+
* of them.
|
|
323
|
+
*/
|
|
324
|
+
createdGhostMembers;
|
|
310
325
|
/** @internal */
|
|
311
326
|
constructor(wire) {
|
|
312
327
|
this.success = wire.success;
|
|
@@ -317,6 +332,11 @@ var TournamentImportResult = class {
|
|
|
317
332
|
this.dryRun = wire.dryRun ?? false;
|
|
318
333
|
this.message = wire.message;
|
|
319
334
|
this.errors = Object.freeze(wire.errors ?? []);
|
|
335
|
+
this.createdGhostMembers = Object.freeze(
|
|
336
|
+
(wire.createdGhostMembers ?? []).map(
|
|
337
|
+
(entry) => Object.freeze({ ref: entry.ref, memberId: entry.memberId })
|
|
338
|
+
)
|
|
339
|
+
);
|
|
320
340
|
Object.freeze(this);
|
|
321
341
|
}
|
|
322
342
|
/** True when the import succeeded without errors. */
|
|
@@ -419,10 +439,22 @@ var SportRating = class {
|
|
|
419
439
|
rating;
|
|
420
440
|
/** Category abbreviation for the primary rating (e.g. `'VO'`). */
|
|
421
441
|
abbr;
|
|
442
|
+
/** Player is VAIRified in this sport (has a verified, non-recreational rating). */
|
|
443
|
+
isVairified;
|
|
444
|
+
/** Player is an active VAIR Pro (can rate) in this sport. Alias of {@link isVairPro}. */
|
|
445
|
+
isRater;
|
|
446
|
+
/** Player is an active VAIR Pro (can rate) in this sport. */
|
|
447
|
+
isVairPro;
|
|
448
|
+
/** VAIR-Pro lifecycle status in this sport (`'ACTIVE'` / `'PENDING'` / `null`). */
|
|
449
|
+
isVairProStatus;
|
|
422
450
|
#splits;
|
|
423
451
|
constructor(wire) {
|
|
424
452
|
this.rating = wire.rating;
|
|
425
453
|
this.abbr = wire.abbr;
|
|
454
|
+
this.isVairified = wire.isVairified ?? false;
|
|
455
|
+
this.isRater = wire.isRater ?? false;
|
|
456
|
+
this.isVairPro = wire.isVairPro ?? false;
|
|
457
|
+
this.isVairProStatus = wire.isVairProStatus ?? null;
|
|
426
458
|
this.#splits = new Map(Object.entries(wire.ratingSplits ?? {}));
|
|
427
459
|
Object.freeze(this);
|
|
428
460
|
}
|
|
@@ -507,6 +539,20 @@ var Member = class {
|
|
|
507
539
|
state;
|
|
508
540
|
zip;
|
|
509
541
|
country;
|
|
542
|
+
/**
|
|
543
|
+
* The DATE this member's VAIR account was created (`YYYY-MM-DD`, UTC), or
|
|
544
|
+
* `null` when the endpoint does not supply it.
|
|
545
|
+
*
|
|
546
|
+
* Present on `members.get()`, `members.getBulk()` and `members.getByEmail()` —
|
|
547
|
+
* the calls where you already know which member you asked about. **Never on
|
|
548
|
+
* `members.search()`**, which is discovery: account age is not something you
|
|
549
|
+
* can browse strangers by.
|
|
550
|
+
*
|
|
551
|
+
* Deliberately a date, not a timestamp. It exists so you can apply a
|
|
552
|
+
* new-accounts-only referral rule — crediting an ambassador only for accounts
|
|
553
|
+
* created because of their event.
|
|
554
|
+
*/
|
|
555
|
+
memberSince;
|
|
510
556
|
gender;
|
|
511
557
|
status;
|
|
512
558
|
sport;
|
|
@@ -525,6 +571,7 @@ var Member = class {
|
|
|
525
571
|
this.state = wire.state ?? null;
|
|
526
572
|
this.zip = wire.zip ?? null;
|
|
527
573
|
this.country = wire.country ?? null;
|
|
574
|
+
this.memberSince = wire.memberSince ?? null;
|
|
528
575
|
this.gender = wire.gender ?? null;
|
|
529
576
|
this.status = Object.freeze({ ...wire.status });
|
|
530
577
|
this.sport = new MemberSportMap(wire.sport);
|
|
@@ -572,6 +619,70 @@ var Member = class {
|
|
|
572
619
|
}
|
|
573
620
|
};
|
|
574
621
|
|
|
622
|
+
// src/models/members-by-email-result.ts
|
|
623
|
+
var MemberEmailMatch = class {
|
|
624
|
+
/** The address exactly as you supplied it, not as stored. */
|
|
625
|
+
email;
|
|
626
|
+
/** Every member holding this address. Never empty. */
|
|
627
|
+
members;
|
|
628
|
+
/** @internal */
|
|
629
|
+
constructor(wire) {
|
|
630
|
+
this.email = wire.email;
|
|
631
|
+
this.members = Object.freeze(wire.members.map((m) => new Member(m)));
|
|
632
|
+
Object.freeze(this);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* The single member for this address, or `null` when the address is
|
|
636
|
+
* ambiguous (more than one match).
|
|
637
|
+
*
|
|
638
|
+
* Use this rather than `members[0]` when a wrong link is worse than no
|
|
639
|
+
* link — it refuses to guess instead of silently picking one.
|
|
640
|
+
*/
|
|
641
|
+
get sole() {
|
|
642
|
+
return this.members.length === 1 ? this.members[0] ?? null : null;
|
|
643
|
+
}
|
|
644
|
+
/** Whether this address resolved to more than one member. */
|
|
645
|
+
get isAmbiguous() {
|
|
646
|
+
return this.members.length > 1;
|
|
647
|
+
}
|
|
648
|
+
toString() {
|
|
649
|
+
return `MemberEmailMatch ${this.email} -> ${this.members.length} member(s)`;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
var MembersByEmailResult = class {
|
|
653
|
+
matched;
|
|
654
|
+
/** Addresses that resolved to nothing, echoed as you supplied them. */
|
|
655
|
+
notFound;
|
|
656
|
+
/** @internal */
|
|
657
|
+
constructor(wire) {
|
|
658
|
+
this.matched = Object.freeze(wire.matched.map((m) => new MemberEmailMatch(m)));
|
|
659
|
+
this.notFound = Object.freeze([...wire.notFound]);
|
|
660
|
+
Object.freeze(this);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Look up one address's match, case-insensitively.
|
|
664
|
+
*
|
|
665
|
+
* Saves callers a linear scan and, more importantly, saves them from
|
|
666
|
+
* matching case-sensitively against an address the server echoed back
|
|
667
|
+
* in whatever case they originally sent.
|
|
668
|
+
*/
|
|
669
|
+
get(email) {
|
|
670
|
+
const needle = email.trim().toLowerCase();
|
|
671
|
+
return this.matched.find((m) => m.email.toLowerCase() === needle) ?? null;
|
|
672
|
+
}
|
|
673
|
+
/** Whether every requested address resolved to at least one member. */
|
|
674
|
+
get allResolved() {
|
|
675
|
+
return this.notFound.length === 0;
|
|
676
|
+
}
|
|
677
|
+
/** Total number of members across every matched address. */
|
|
678
|
+
get memberCount() {
|
|
679
|
+
return this.matched.reduce((n, m) => n + m.members.length, 0);
|
|
680
|
+
}
|
|
681
|
+
toString() {
|
|
682
|
+
return `MembersByEmailResult matched=${this.matched.length} notFound=${this.notFound.length}`;
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
575
686
|
// src/models/rating-update.ts
|
|
576
687
|
var RatingUpdate = class {
|
|
577
688
|
memberId;
|
|
@@ -620,6 +731,7 @@ var RatingUpdate = class {
|
|
|
620
731
|
// src/resources/members.ts
|
|
621
732
|
var DEFAULT_PAGE_SIZE = 20;
|
|
622
733
|
var MAX_PAGE_SIZE = 100;
|
|
734
|
+
var MAX_EMAILS_PER_LOOKUP = 100;
|
|
623
735
|
var MembersResource = class {
|
|
624
736
|
#http;
|
|
625
737
|
/** @internal */
|
|
@@ -761,6 +873,70 @@ var MembersResource = class {
|
|
|
761
873
|
});
|
|
762
874
|
return rows.map((row) => new Member(row));
|
|
763
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* Resolve up to 100 members by their **exact** email address.
|
|
878
|
+
*
|
|
879
|
+
* Use this to link your users to their VAIR identity when you hold
|
|
880
|
+
* their email but not their member ID — e.g. resolving a tournament
|
|
881
|
+
* roster at registration instead of waiting for each player to
|
|
882
|
+
* complete SSO.
|
|
883
|
+
*
|
|
884
|
+
* **Requires the `key:player:lookup` scope**, which is granted per
|
|
885
|
+
* partner on approval. Holding `key:player:search` does not imply it.
|
|
886
|
+
*
|
|
887
|
+
* Matching is exact and case-insensitive; there is deliberately no
|
|
888
|
+
* partial, prefix or fuzzy matching. Every address you supply comes
|
|
889
|
+
* back in either `matched` or `notFound`, so read `notFound` directly
|
|
890
|
+
* instead of diffing your input against the results.
|
|
891
|
+
*
|
|
892
|
+
* A `notFound` address is **not** proof the person has no VAIR
|
|
893
|
+
* account — unclaimed imported records are excluded from this lookup.
|
|
894
|
+
*
|
|
895
|
+
* @param emails - Email addresses to resolve (max 100).
|
|
896
|
+
* @param options - Optional filters.
|
|
897
|
+
* @param options.sport - Sport code to scope ratings (e.g. `'pickleball'`).
|
|
898
|
+
* @throws {@link ValidationError} If more than 100 addresses are
|
|
899
|
+
* provided, or the list is empty.
|
|
900
|
+
* @category Members
|
|
901
|
+
*
|
|
902
|
+
* @example
|
|
903
|
+
* ```ts
|
|
904
|
+
* const result = await client.members.getByEmail([
|
|
905
|
+
* 'ada@example.com',
|
|
906
|
+
* 'nobody@example.com',
|
|
907
|
+
* ]);
|
|
908
|
+
*
|
|
909
|
+
* for (const match of result.matched) {
|
|
910
|
+
* const member = match.sole; // null when the address is ambiguous
|
|
911
|
+
* if (member) console.log(match.email, '->', member.memberId);
|
|
912
|
+
* }
|
|
913
|
+
*
|
|
914
|
+
* console.log('no VAIR account found for:', result.notFound);
|
|
915
|
+
* ```
|
|
916
|
+
*/
|
|
917
|
+
async getByEmail(emails, options) {
|
|
918
|
+
if (emails.length === 0) {
|
|
919
|
+
throw new ValidationError("At least one email address is required");
|
|
920
|
+
}
|
|
921
|
+
if (emails.length > MAX_EMAILS_PER_LOOKUP) {
|
|
922
|
+
throw new ValidationError(`Maximum ${MAX_EMAILS_PER_LOOKUP} email addresses per request`);
|
|
923
|
+
}
|
|
924
|
+
const offender = emails.find((e) => e.includes(","));
|
|
925
|
+
if (offender !== void 0) {
|
|
926
|
+
throw new ValidationError(`Email address must not contain a comma: ${offender}`);
|
|
927
|
+
}
|
|
928
|
+
const query = { emails: emails.join(",") };
|
|
929
|
+
if (options?.sport) query.sport = options.sport;
|
|
930
|
+
const wire = await this.#http.request({
|
|
931
|
+
method: "GET",
|
|
932
|
+
path: "/partner/members/by-email",
|
|
933
|
+
query
|
|
934
|
+
});
|
|
935
|
+
return new MembersByEmailResult({
|
|
936
|
+
matched: wire?.matched ?? [],
|
|
937
|
+
notFound: wire?.notFound ?? []
|
|
938
|
+
});
|
|
939
|
+
}
|
|
764
940
|
/**
|
|
765
941
|
* Poll for rating change notifications.
|
|
766
942
|
*
|
|
@@ -873,13 +1049,31 @@ function describeScopes(scopes) {
|
|
|
873
1049
|
}));
|
|
874
1050
|
}
|
|
875
1051
|
function generateState() {
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1052
|
+
const webCrypto = globalThis.crypto;
|
|
1053
|
+
if (typeof webCrypto?.getRandomValues !== "function") {
|
|
1054
|
+
throw new Error(
|
|
1055
|
+
"generateState() needs Web Crypto (crypto.getRandomValues), which is unavailable in this runtime. Node 19+ and browsers have it built in; on React Native / Hermes, install 'react-native-get-random-values' and import it once at your app entry before using the SDK \u2014 or pass your own high-entropy `state` string to oauth.authorize()."
|
|
1056
|
+
);
|
|
881
1057
|
}
|
|
882
|
-
|
|
1058
|
+
const bytes = new Uint8Array(32);
|
|
1059
|
+
webCrypto.getRandomValues(bytes);
|
|
1060
|
+
return base64UrlNoPad(bytes);
|
|
1061
|
+
}
|
|
1062
|
+
var B64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
1063
|
+
function base64UrlNoPad(bytes) {
|
|
1064
|
+
let out = "";
|
|
1065
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
1066
|
+
const b0 = bytes[i] ?? 0;
|
|
1067
|
+
const b1 = bytes[i + 1] ?? 0;
|
|
1068
|
+
const b2 = bytes[i + 2] ?? 0;
|
|
1069
|
+
const hasB1 = i + 1 < bytes.length;
|
|
1070
|
+
const hasB2 = i + 2 < bytes.length;
|
|
1071
|
+
out += B64URL_ALPHABET[b0 >> 2] ?? "";
|
|
1072
|
+
out += B64URL_ALPHABET[(b0 & 3) << 4 | b1 >> 4] ?? "";
|
|
1073
|
+
if (hasB1) out += B64URL_ALPHABET[(b1 & 15) << 2 | b2 >> 6] ?? "";
|
|
1074
|
+
if (hasB2) out += B64URL_ALPHABET[b2 & 63] ?? "";
|
|
1075
|
+
}
|
|
1076
|
+
return out;
|
|
883
1077
|
}
|
|
884
1078
|
function ensureProfileRead(scopes) {
|
|
885
1079
|
if (scopes.includes("user:profile:read")) {
|
|
@@ -981,6 +1175,207 @@ function tokenResponseFromWire(data) {
|
|
|
981
1175
|
};
|
|
982
1176
|
}
|
|
983
1177
|
|
|
1178
|
+
// src/models/attribution.ts
|
|
1179
|
+
var MemberAttribution = class {
|
|
1180
|
+
memberId;
|
|
1181
|
+
/** True when some ambassador already holds credit for this member. */
|
|
1182
|
+
attributed;
|
|
1183
|
+
/**
|
|
1184
|
+
* The member id of the ambassador holding the credit.
|
|
1185
|
+
*
|
|
1186
|
+
* Compare it against your own event host's member id to tell "already credited
|
|
1187
|
+
* to my host — nothing to do" from "credited to somebody else — a person needs
|
|
1188
|
+
* to look, because claiming it takes credit from them".
|
|
1189
|
+
*
|
|
1190
|
+
* `null` both when nobody holds credit and when credit is held by a record that
|
|
1191
|
+
* has no member id of its own, so check {@link attributed} to tell those apart.
|
|
1192
|
+
*/
|
|
1193
|
+
ambassadorMemberId;
|
|
1194
|
+
/** The date the credit was established (`YYYY-MM-DD`), or `null`. */
|
|
1195
|
+
attributedAt;
|
|
1196
|
+
/** @internal */
|
|
1197
|
+
constructor(wire) {
|
|
1198
|
+
this.memberId = wire.memberId;
|
|
1199
|
+
this.attributed = wire.attributed;
|
|
1200
|
+
this.ambassadorMemberId = wire.ambassadorMemberId ?? null;
|
|
1201
|
+
this.attributedAt = wire.attributedAt ?? null;
|
|
1202
|
+
Object.freeze(this);
|
|
1203
|
+
}
|
|
1204
|
+
/** True when nobody holds credit yet, so this member can be claimed. */
|
|
1205
|
+
get isClaimable() {
|
|
1206
|
+
return !this.attributed;
|
|
1207
|
+
}
|
|
1208
|
+
/** True when credit is held by an ambassador OTHER than the one given. */
|
|
1209
|
+
heldBySomeoneOtherThan(ambassadorMemberId) {
|
|
1210
|
+
return this.attributed && this.ambassadorMemberId !== ambassadorMemberId;
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
var MembersAttributionResult = class {
|
|
1214
|
+
attributions;
|
|
1215
|
+
/**
|
|
1216
|
+
* Member ids that matched no member. Read this rather than diffing your input
|
|
1217
|
+
* against the results — every id you sent lands in one bucket or the other.
|
|
1218
|
+
*/
|
|
1219
|
+
notFound;
|
|
1220
|
+
/** @internal */
|
|
1221
|
+
constructor(wire) {
|
|
1222
|
+
this.attributions = Object.freeze(
|
|
1223
|
+
(wire.attributions ?? []).map((a) => new MemberAttribution(a))
|
|
1224
|
+
);
|
|
1225
|
+
this.notFound = Object.freeze([...wire.notFound ?? []]);
|
|
1226
|
+
Object.freeze(this);
|
|
1227
|
+
}
|
|
1228
|
+
/** Attribution for one member id, or `undefined` if it was not returned. */
|
|
1229
|
+
get(memberId) {
|
|
1230
|
+
return this.attributions.find((a) => a.memberId === memberId);
|
|
1231
|
+
}
|
|
1232
|
+
/** Members nobody holds credit for yet. */
|
|
1233
|
+
get claimable() {
|
|
1234
|
+
return this.attributions.filter((a) => a.isClaimable);
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
var AttributionResult = class {
|
|
1238
|
+
/** How many members were newly attributed by this request. */
|
|
1239
|
+
attributed;
|
|
1240
|
+
/** One entry per member id supplied, in the order supplied. */
|
|
1241
|
+
results;
|
|
1242
|
+
/** @internal */
|
|
1243
|
+
constructor(wire) {
|
|
1244
|
+
this.attributed = wire.attributed;
|
|
1245
|
+
this.results = Object.freeze(
|
|
1246
|
+
(wire.results ?? []).map((r) => Object.freeze({ memberId: r.memberId, outcome: r.outcome }))
|
|
1247
|
+
);
|
|
1248
|
+
Object.freeze(this);
|
|
1249
|
+
}
|
|
1250
|
+
/** Member ids with the given outcome. */
|
|
1251
|
+
withOutcome(outcome) {
|
|
1252
|
+
return this.results.filter((r) => r.outcome === outcome).map((r) => r.memberId);
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Members already credited to somebody. These are the ones worth a human
|
|
1256
|
+
* look — it may be your own host, or it may be another ambassador.
|
|
1257
|
+
*/
|
|
1258
|
+
get alreadyAttributed() {
|
|
1259
|
+
return this.withOutcome("already_attributed");
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Members rejected because their account pre-dates the event's registration
|
|
1263
|
+
* page. The event did not recruit them, so no credit is due.
|
|
1264
|
+
*/
|
|
1265
|
+
get predatedEvent() {
|
|
1266
|
+
return this.withOutcome("account_predates_event");
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
// src/resources/referrals.ts
|
|
1271
|
+
var MAX_IDS_PER_READ = 100;
|
|
1272
|
+
var MAX_IDS_PER_WRITE = 500;
|
|
1273
|
+
var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1274
|
+
var ReferralsResource = class {
|
|
1275
|
+
#http;
|
|
1276
|
+
/** @internal */
|
|
1277
|
+
constructor(http) {
|
|
1278
|
+
this.#http = http;
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Who currently earns referral credit for these members.
|
|
1282
|
+
*
|
|
1283
|
+
* Use this before {@link attribute} to tell the two cases apart that matter:
|
|
1284
|
+
* a member already credited to your own event host (nothing to do) and one
|
|
1285
|
+
* credited to a different ambassador (a person should look, because claiming
|
|
1286
|
+
* it takes credit from them).
|
|
1287
|
+
*
|
|
1288
|
+
* @example
|
|
1289
|
+
* ```ts
|
|
1290
|
+
* const result = await client.referrals.get([4873327, 4873328]);
|
|
1291
|
+
*
|
|
1292
|
+
* for (const a of result.claimable) {
|
|
1293
|
+
* console.log(a.memberId, 'has no credit yet');
|
|
1294
|
+
* }
|
|
1295
|
+
* console.log('no such member:', result.notFound);
|
|
1296
|
+
* ```
|
|
1297
|
+
*
|
|
1298
|
+
* @throws {@link ValidationError} If the list is empty or exceeds 100 ids.
|
|
1299
|
+
*/
|
|
1300
|
+
async get(memberIds) {
|
|
1301
|
+
if (memberIds.length === 0) {
|
|
1302
|
+
throw new ValidationError("At least one member id is required");
|
|
1303
|
+
}
|
|
1304
|
+
if (memberIds.length > MAX_IDS_PER_READ) {
|
|
1305
|
+
throw new ValidationError(`Maximum ${MAX_IDS_PER_READ} member ids per request`);
|
|
1306
|
+
}
|
|
1307
|
+
const wire = await this.#http.request({
|
|
1308
|
+
method: "GET",
|
|
1309
|
+
path: "/partner/members/attribution",
|
|
1310
|
+
query: { memberIds: memberIds.join(",") }
|
|
1311
|
+
});
|
|
1312
|
+
return new MembersAttributionResult({
|
|
1313
|
+
attributions: wire?.attributions ?? [],
|
|
1314
|
+
notFound: wire?.notFound ?? []
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Credit an ambassador for players their event recruited.
|
|
1319
|
+
*
|
|
1320
|
+
* `registrationPublishedAt` is the date the event's registration page was
|
|
1321
|
+
* **first published**. Accounts created before it did not come from the event
|
|
1322
|
+
* and are rejected with `account_predates_event`. VAIR applies that rule
|
|
1323
|
+
* itself, so every partner is held to the same one.
|
|
1324
|
+
*
|
|
1325
|
+
* VAIR cannot verify the date — it holds no record of your registration pages —
|
|
1326
|
+
* so the value you send is recorded for audit. Send the real one.
|
|
1327
|
+
*
|
|
1328
|
+
* Safe to retry: attribution is one-per-player forever, enforced by the
|
|
1329
|
+
* database, so a resubmitted player returns `already_attributed` and nothing
|
|
1330
|
+
* changes.
|
|
1331
|
+
*
|
|
1332
|
+
* @example
|
|
1333
|
+
* ```ts
|
|
1334
|
+
* const result = await client.referrals.attribute({
|
|
1335
|
+
* referralCode: 'hillhurst-open',
|
|
1336
|
+
* registrationPublishedAt: '2026-08-01',
|
|
1337
|
+
* memberIds: [4873327, 4873328],
|
|
1338
|
+
* });
|
|
1339
|
+
*
|
|
1340
|
+
* console.log(result.attributed, 'newly credited');
|
|
1341
|
+
* console.log('need a human:', result.alreadyAttributed);
|
|
1342
|
+
* console.log('too old to credit:', result.predatedEvent);
|
|
1343
|
+
* ```
|
|
1344
|
+
*
|
|
1345
|
+
* @throws {@link ValidationError} If the list is empty or exceeds 500 ids, or
|
|
1346
|
+
* the date is not `YYYY-MM-DD`.
|
|
1347
|
+
*/
|
|
1348
|
+
async attribute(input) {
|
|
1349
|
+
if (!input.referralCode.trim()) {
|
|
1350
|
+
throw new ValidationError("A referral code is required");
|
|
1351
|
+
}
|
|
1352
|
+
if (!ISO_DATE.test(input.registrationPublishedAt)) {
|
|
1353
|
+
throw new ValidationError(
|
|
1354
|
+
"registrationPublishedAt must be a calendar date formatted YYYY-MM-DD"
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
if (input.memberIds.length === 0) {
|
|
1358
|
+
throw new ValidationError("At least one member id is required");
|
|
1359
|
+
}
|
|
1360
|
+
if (input.memberIds.length > MAX_IDS_PER_WRITE) {
|
|
1361
|
+
throw new ValidationError(`Maximum ${MAX_IDS_PER_WRITE} member ids per request`);
|
|
1362
|
+
}
|
|
1363
|
+
const wire = await this.#http.request({
|
|
1364
|
+
method: "POST",
|
|
1365
|
+
path: "/partner/ambassador/attribution",
|
|
1366
|
+
body: {
|
|
1367
|
+
referralCode: input.referralCode.trim(),
|
|
1368
|
+
registrationPublishedAt: input.registrationPublishedAt,
|
|
1369
|
+
memberIds: [...input.memberIds]
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
return new AttributionResult({
|
|
1373
|
+
attributed: wire?.attributed ?? 0,
|
|
1374
|
+
results: wire?.results ?? []
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
|
|
984
1379
|
// src/models/webhook-delivery.ts
|
|
985
1380
|
var WebhookDelivery = class {
|
|
986
1381
|
id;
|
|
@@ -1081,6 +1476,14 @@ var ENVIRONMENTS = Object.freeze({
|
|
|
1081
1476
|
local: "http://localhost:3001/api/v1"
|
|
1082
1477
|
});
|
|
1083
1478
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1479
|
+
function readEnv(name) {
|
|
1480
|
+
try {
|
|
1481
|
+
if (typeof process === "undefined") return void 0;
|
|
1482
|
+
return process.env?.[name];
|
|
1483
|
+
} catch {
|
|
1484
|
+
return void 0;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1084
1487
|
var Vairified = class {
|
|
1085
1488
|
/** The resolved API key this client is using. */
|
|
1086
1489
|
apiKey;
|
|
@@ -1100,9 +1503,14 @@ var Vairified = class {
|
|
|
1100
1503
|
leaderboard;
|
|
1101
1504
|
/** Webhook delivery inspection — deliveries. */
|
|
1102
1505
|
webhooks;
|
|
1506
|
+
/**
|
|
1507
|
+
* Read and record ambassador referral credit. Each method needs its own
|
|
1508
|
+
* per-partner permission — see {@link ReferralsResource}.
|
|
1509
|
+
*/
|
|
1510
|
+
referrals;
|
|
1103
1511
|
#transport;
|
|
1104
1512
|
constructor(options = {}) {
|
|
1105
|
-
const apiKey = options.apiKey ??
|
|
1513
|
+
const apiKey = options.apiKey ?? readEnv("VAIRIFIED_API_KEY") ?? "";
|
|
1106
1514
|
if (apiKey.length === 0) {
|
|
1107
1515
|
throw new Error("API key required. Pass { apiKey } or set VAIRIFIED_API_KEY.");
|
|
1108
1516
|
}
|
|
@@ -1111,7 +1519,7 @@ var Vairified = class {
|
|
|
1111
1519
|
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
1112
1520
|
this.env = options.env ?? "production";
|
|
1113
1521
|
} else {
|
|
1114
|
-
const envName = options.env ??
|
|
1522
|
+
const envName = options.env ?? readEnv("VAIRIFIED_ENV") ?? "production";
|
|
1115
1523
|
if (options.env && !(envName in ENVIRONMENTS)) {
|
|
1116
1524
|
throw new Error(
|
|
1117
1525
|
`Unknown environment: ${envName}. Use one of: ${Object.keys(ENVIRONMENTS).join(", ")}`
|
|
@@ -1132,6 +1540,7 @@ var Vairified = class {
|
|
|
1132
1540
|
this.oauth = new OAuthResource(this.#transport);
|
|
1133
1541
|
this.leaderboard = new LeaderboardResource(this.#transport);
|
|
1134
1542
|
this.webhooks = new WebhooksResource(this.#transport);
|
|
1543
|
+
this.referrals = new ReferralsResource(this.#transport);
|
|
1135
1544
|
}
|
|
1136
1545
|
/**
|
|
1137
1546
|
* API usage statistics for the current API key.
|
|
@@ -1175,7 +1584,9 @@ var Vairified = class {
|
|
|
1175
1584
|
MatchBatchResult,
|
|
1176
1585
|
MatchesResource,
|
|
1177
1586
|
Member,
|
|
1587
|
+
MemberEmailMatch,
|
|
1178
1588
|
MemberSportMap,
|
|
1589
|
+
MembersByEmailResult,
|
|
1179
1590
|
MembersResource,
|
|
1180
1591
|
NotFoundError,
|
|
1181
1592
|
OAuthError,
|