vairified 0.7.0 → 0.8.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 +142 -0
- package/dist/index.cjs +227 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +483 -4
- package/dist/index.d.ts +483 -4
- package/dist/index.js +216 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -466,6 +466,240 @@ interface AttributionResultWire {
|
|
|
466
466
|
readonly outcome: AttributionOutcome;
|
|
467
467
|
}[];
|
|
468
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* A value the API documents today, without closing the set.
|
|
471
|
+
*
|
|
472
|
+
* `'a' | 'b' | (string & {})` keeps editor autocomplete for the known values
|
|
473
|
+
* while still accepting one we have not seen. That matters more here than
|
|
474
|
+
* anywhere else in this SDK: webhook payloads are produced by a service that
|
|
475
|
+
* ships independently of this package, so a closed union is wrong the moment
|
|
476
|
+
* the backend adds a value — and it fails at the worst possible time, in a
|
|
477
|
+
* partner's live handler rather than at their compile step.
|
|
478
|
+
*
|
|
479
|
+
* Worked example: `connection.revoked` carried exactly one `reason` for its
|
|
480
|
+
* whole life, and a second (`player_disconnected`) landed while this very file
|
|
481
|
+
* was being written. A closed union would have shipped broken.
|
|
482
|
+
*
|
|
483
|
+
* @category Webhooks
|
|
484
|
+
*/
|
|
485
|
+
type OpenEnum<Known extends string> = Known | (string & {});
|
|
486
|
+
/** One sport's VAIR Pro standing, as carried by a `member.status` event. */
|
|
487
|
+
interface MemberStatusEventSportWire {
|
|
488
|
+
/**
|
|
489
|
+
* Whether the member is an **active** VAIR Pro (certified rater) **in this
|
|
490
|
+
* sport** — the field to check before letting someone rate.
|
|
491
|
+
*
|
|
492
|
+
* :rotating_light: **`false` means "holds one, awaiting approval", not
|
|
493
|
+
* "may rate".** Treat only `true` as permission. A member certified in
|
|
494
|
+
* pickleball is not thereby certified in padel.
|
|
495
|
+
*/
|
|
496
|
+
readonly isVairPro?: boolean;
|
|
497
|
+
/** Documented alias of {@link isVairPro}, matching `GET /partner/member`. */
|
|
498
|
+
readonly isRater?: boolean;
|
|
499
|
+
readonly isVairProStatus?: OpenEnum<'ACTIVE' | 'PENDING'>;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* The `data` block of a `member.status` event.
|
|
503
|
+
*
|
|
504
|
+
* :warning: `sports` is **absent, not empty**, for a member who did not grant
|
|
505
|
+
* `user:rating:read`. Absence means "we were not permitted to tell you", which
|
|
506
|
+
* is a different claim from "holds no certifications" — check for the key
|
|
507
|
+
* before concluding anything about a member's rater standing.
|
|
508
|
+
*
|
|
509
|
+
* :warning: A sport appears only while the member holds a **current**
|
|
510
|
+
* certification in it. Expired and revoked certifications are not reported, so
|
|
511
|
+
* a lapsed rater is indistinguishable here from someone who was never
|
|
512
|
+
* certified. Do not use this map to answer "have they ever been a rater?".
|
|
513
|
+
*/
|
|
514
|
+
interface MemberStatusEventDataWire {
|
|
515
|
+
readonly memberId: number;
|
|
516
|
+
readonly isVairPlus: boolean;
|
|
517
|
+
readonly isAmbassador: boolean;
|
|
518
|
+
readonly sports?: Record<string, MemberStatusEventSportWire> | null;
|
|
519
|
+
/**
|
|
520
|
+
* VAIR Pro standing **collapsed across every sport**: `ACTIVE` if any sport's
|
|
521
|
+
* certification is active, else `PENDING` if any is pending, else `null`.
|
|
522
|
+
*
|
|
523
|
+
* :rotating_light: **This cannot answer "may this person rate my padel
|
|
524
|
+
* event".** It is `ACTIVE` when the member is certified in *any* sport, so
|
|
525
|
+
* using it as a per-sport permission grants a pickleball rater authority over
|
|
526
|
+
* padel. Read {@link MemberStatusEventDataWire.sports} where the sport
|
|
527
|
+
* matters; this field exists for partners who consumed it before `sports`
|
|
528
|
+
* did, and for the GoHighLevel contact sync that mirrors it.
|
|
529
|
+
*/
|
|
530
|
+
readonly vairProStatus: OpenEnum<'ACTIVE' | 'PENDING'> | null;
|
|
531
|
+
readonly vairifiedRatingStatus: OpenEnum<'NONE' | 'PENDING_PAYMENT' | 'PAID' | 'COMPLETED'>;
|
|
532
|
+
readonly changedAt: string;
|
|
533
|
+
/**
|
|
534
|
+
* Monotonic ordering token, compared only against other values for the SAME
|
|
535
|
+
* member. **Not currently emitted by the API** — treat an absent value as
|
|
536
|
+
* "cannot be ordered", never as zero, and order by `changedAt` until it
|
|
537
|
+
* appears.
|
|
538
|
+
*/
|
|
539
|
+
readonly sequence?: string;
|
|
540
|
+
}
|
|
541
|
+
/** The `data` block of a `connection.revoked` event. */
|
|
542
|
+
interface ConnectionRevokedEventDataWire {
|
|
543
|
+
readonly memberId: number | null;
|
|
544
|
+
readonly reason: OpenEnum<'player_deleted' | 'player_disconnected'>;
|
|
545
|
+
readonly revokedAt: string;
|
|
546
|
+
}
|
|
547
|
+
/** Fields every webhook event carries, whatever its type. */
|
|
548
|
+
interface WebhookEventEnvelopeWire {
|
|
549
|
+
readonly event: string;
|
|
550
|
+
/** Stable event id. **Deduplicate on this** — delivery is at-least-once. */
|
|
551
|
+
readonly eventId: string;
|
|
552
|
+
readonly timestamp: string;
|
|
553
|
+
}
|
|
554
|
+
interface MemberStatusEventWire extends WebhookEventEnvelopeWire {
|
|
555
|
+
readonly event: 'member.status';
|
|
556
|
+
readonly data: MemberStatusEventDataWire;
|
|
557
|
+
}
|
|
558
|
+
interface ConnectionRevokedEventWire extends WebhookEventEnvelopeWire {
|
|
559
|
+
readonly event: 'connection.revoked';
|
|
560
|
+
readonly data: ConnectionRevokedEventDataWire;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* The `data` block of a `rating.updated` delivery.
|
|
564
|
+
*
|
|
565
|
+
* :rotating_light: **This is NOT {@link PartnerRatingUpdateWire}.** That type is
|
|
566
|
+
* the shape `GET /partner/rating-updates` returns when you *poll*, and it still
|
|
567
|
+
* carries the old per-sport diff (`previousRating` / `newRating`). The webhook
|
|
568
|
+
* has sent a **full multi-sport snapshot** since Vairified#899, and the two have
|
|
569
|
+
* not matched since. Reusing the poll type here is a mistake this SDK made once
|
|
570
|
+
* and the reason this docstring exists.
|
|
571
|
+
*
|
|
572
|
+
* :warning: **Two variants, distinguished by {@link ratingDataWithheld}.** An app
|
|
573
|
+
* holding `user:webhook:subscribe` but not `user:rating:read` is told *that* a
|
|
574
|
+
* member's rating changed and not *what it changed to* — `sports` is **absent**,
|
|
575
|
+
* not empty, and `ratingDataWithheld` is `true`.
|
|
576
|
+
*/
|
|
577
|
+
/**
|
|
578
|
+
* One sport's rating standing inside a `rating.updated` snapshot.
|
|
579
|
+
*
|
|
580
|
+
* :rotating_light: **Deliberately NOT {@link SportRatingWire}.** That type is the
|
|
581
|
+
* POLLING shape: it declares `rating`, `abbr` and `ratingSplits` required and
|
|
582
|
+
* closes `isVairProStatus` to two values. Reusing it here would let a partner
|
|
583
|
+
* write an exhaustive switch the compiler accepts and the API then outgrows, and
|
|
584
|
+
* would contradict the Python SDK, which types every field optional. Everything
|
|
585
|
+
* here is open and optional for the same reason the enums are.
|
|
586
|
+
*/
|
|
587
|
+
interface RatingUpdatedSportWire {
|
|
588
|
+
readonly rating?: number;
|
|
589
|
+
readonly abbr?: string;
|
|
590
|
+
readonly ratingSplits?: Readonly<Record<string, RatingSplitWire>>;
|
|
591
|
+
readonly isVairified?: boolean;
|
|
592
|
+
readonly isRater?: boolean;
|
|
593
|
+
readonly isVairPro?: boolean;
|
|
594
|
+
readonly isVairProStatus?: OpenEnum<'ACTIVE' | 'PENDING'>;
|
|
595
|
+
}
|
|
596
|
+
interface RatingUpdatedEventDataWire {
|
|
597
|
+
readonly memberId: number;
|
|
598
|
+
/**
|
|
599
|
+
* The member's complete rating standing, keyed by sport code, at the moment
|
|
600
|
+
* the computation finished — a **snapshot**, not a diff.
|
|
601
|
+
*
|
|
602
|
+
* :rotating_light: **Absent when the partner lacks `user:rating:read`.** Absence
|
|
603
|
+
* means "we were not permitted to tell you", which is not the same claim as
|
|
604
|
+
* "no ratings". Check {@link ratingDataWithheld}.
|
|
605
|
+
*/
|
|
606
|
+
readonly sports?: Readonly<Record<string, RatingUpdatedSportWire>> | null;
|
|
607
|
+
/** When the rating computation that produced this state completed (ISO 8601). */
|
|
608
|
+
readonly changedAt: string;
|
|
609
|
+
/**
|
|
610
|
+
* Monotonic ordering token. **Use it to discard stale deliveries.**
|
|
611
|
+
*
|
|
612
|
+
* :rotating_light: Because the payload is a **snapshot** rather than a diff, it
|
|
613
|
+
* is order-dependent in a way the old diff payload was not. Deliveries run
|
|
614
|
+
* concurrently with independent retry backoff, so two events for one member
|
|
615
|
+
* **can arrive out of order** — and applying the older one last leaves you
|
|
616
|
+
* holding a rating the member no longer has.
|
|
617
|
+
*
|
|
618
|
+
* :rotating_light: **Compare it as an INTEGER, never as a string.** It is an
|
|
619
|
+
* unpadded decimal, so a string comparison is lexicographic and
|
|
620
|
+
* `'10000000' > '9999999'` is `false`. At every power-of-ten crossing a
|
|
621
|
+
* string-comparing receiver would discard every later delivery for that
|
|
622
|
+
* member, permanently, and their rating would freeze at the stale value —
|
|
623
|
+
* which is the exact failure this field exists to prevent. Use
|
|
624
|
+
* `BigInt(a) > BigInt(b)`.
|
|
625
|
+
*
|
|
626
|
+
* So: keep the highest `sequence` you have applied **per member**, compared as
|
|
627
|
+
* an integer, and discard any delivery whose value is lower. Compare it only against other values
|
|
628
|
+
* **for the same member** — it is drawn from a platform-wide counter, so gaps
|
|
629
|
+
* carry no meaning and values are not comparable across members. Sent as a
|
|
630
|
+
* string because the value exceeds the safe integer range in some languages.
|
|
631
|
+
*
|
|
632
|
+
* Unlike `member.status`, where this field is declared but not yet emitted,
|
|
633
|
+
* `rating.updated` carries it on **every** delivery, on both variants.
|
|
634
|
+
*/
|
|
635
|
+
readonly sequence: string;
|
|
636
|
+
/**
|
|
637
|
+
* Present and `true` only on the notification variant — the partner was not
|
|
638
|
+
* granted `user:rating:read`, so {@link sports} is absent by design rather
|
|
639
|
+
* than because nothing changed.
|
|
640
|
+
*/
|
|
641
|
+
readonly ratingDataWithheld?: boolean;
|
|
642
|
+
}
|
|
643
|
+
interface RatingUpdatedEventWire extends WebhookEventEnvelopeWire {
|
|
644
|
+
readonly event: 'rating.updated';
|
|
645
|
+
readonly data: RatingUpdatedEventDataWire;
|
|
646
|
+
}
|
|
647
|
+
/** The club an event belongs to, when it has one. */
|
|
648
|
+
interface EventCreatedClubWire {
|
|
649
|
+
readonly name: string;
|
|
650
|
+
readonly city: string | null;
|
|
651
|
+
readonly state: string | null;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* The `data` block of an `event.created` delivery.
|
|
655
|
+
*
|
|
656
|
+
* :warning: **`data.eventId` is a NUMBER and is not the envelope's `eventId`.**
|
|
657
|
+
* The envelope's is the delivery's own string id, used for deduplication; this
|
|
658
|
+
* one is the event's public number, the id a partner uses to refer to the event
|
|
659
|
+
* itself. They shadow each other by name and share nothing else.
|
|
660
|
+
*
|
|
661
|
+
* This is the only **app-scoped** event — it concerns no particular member, so
|
|
662
|
+
* it carries no `memberId`.
|
|
663
|
+
*/
|
|
664
|
+
interface EventCreatedEventDataWire {
|
|
665
|
+
readonly eventId: number;
|
|
666
|
+
readonly name: string;
|
|
667
|
+
readonly type: string;
|
|
668
|
+
readonly status: string;
|
|
669
|
+
readonly sport: string;
|
|
670
|
+
readonly startDate: string | null;
|
|
671
|
+
readonly endDate: string | null;
|
|
672
|
+
readonly club: EventCreatedClubWire | null;
|
|
673
|
+
readonly hostName: string | null;
|
|
674
|
+
readonly winScore: number | null;
|
|
675
|
+
readonly winBy: number | null;
|
|
676
|
+
readonly isPrivate: boolean;
|
|
677
|
+
readonly maxSpots: number | null;
|
|
678
|
+
readonly maxTeams: number | null;
|
|
679
|
+
readonly createdBy: string | null;
|
|
680
|
+
readonly createdAt: string;
|
|
681
|
+
}
|
|
682
|
+
interface EventCreatedEventWire extends WebhookEventEnvelopeWire {
|
|
683
|
+
readonly event: 'event.created';
|
|
684
|
+
readonly data: EventCreatedEventDataWire;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Any event this SDK version does not model richly.
|
|
688
|
+
*
|
|
689
|
+
* Verification succeeds and `data` is handed over untouched. This is
|
|
690
|
+
* deliberate: the signature is checked *before* the body is typed, so an
|
|
691
|
+
* unrecognised event that reaches your handler is an authenticated one. If
|
|
692
|
+
* this threw instead, every partner would break the day the API adds an event.
|
|
693
|
+
*/
|
|
694
|
+
interface UnknownWebhookEventWire extends WebhookEventEnvelopeWire {
|
|
695
|
+
readonly data: unknown;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* A verified webhook event. Narrow on `event` to reach the typed members.
|
|
699
|
+
*
|
|
700
|
+
* @category Webhooks
|
|
701
|
+
*/
|
|
702
|
+
type VerifiedWebhookEvent = MemberStatusEventWire | ConnectionRevokedEventWire | RatingUpdatedEventWire | EventCreatedEventWire | UnknownWebhookEventWire;
|
|
469
703
|
|
|
470
704
|
/**
|
|
471
705
|
* {@link LeaderboardResource} — read-only leaderboard queries.
|
|
@@ -889,9 +1123,14 @@ declare class MembersByEmailResult {
|
|
|
889
1123
|
/**
|
|
890
1124
|
* A single rating change notification.
|
|
891
1125
|
*
|
|
892
|
-
* Returned by {@link MembersResource.ratingUpdates}
|
|
893
|
-
*
|
|
894
|
-
*
|
|
1126
|
+
* Returned by {@link MembersResource.ratingUpdates} — the **polling** endpoint,
|
|
1127
|
+
* and only that.
|
|
1128
|
+
*
|
|
1129
|
+
* :rotating_light: **This is NOT the `rating.updated` webhook payload.** It was,
|
|
1130
|
+
* once; the webhook has sent a full multi-sport snapshot since Vairified#899
|
|
1131
|
+
* and the two shapes have not matched since. The webhook's is
|
|
1132
|
+
* {@link RatingUpdatedEventDataWire}, reachable through `verifyWebhook`. This
|
|
1133
|
+
* sentence used to claim otherwise, and a later change believed it.
|
|
895
1134
|
*
|
|
896
1135
|
* @category Members
|
|
897
1136
|
*/
|
|
@@ -1679,5 +1918,245 @@ declare class OAuthError extends VairifiedError {
|
|
|
1679
1918
|
readonly errorCode?: string;
|
|
1680
1919
|
constructor(message?: string, errorCode?: string, response?: unknown);
|
|
1681
1920
|
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Why a webhook was refused. Lets a handler distinguish "someone is forging
|
|
1923
|
+
* requests" from "our clock drifted" without parsing an error message.
|
|
1924
|
+
*
|
|
1925
|
+
* @category Errors
|
|
1926
|
+
*/
|
|
1927
|
+
type WebhookRejectionReason =
|
|
1928
|
+
/**
|
|
1929
|
+
* No usable signing secret was supplied to {@link verifyWebhook}.
|
|
1930
|
+
*
|
|
1931
|
+
* This is **your** configuration, not an attack — almost always an unset
|
|
1932
|
+
* environment variable. It is separate from {@link signature_mismatch} on
|
|
1933
|
+
* purpose: reporting a missing secret as a mismatch sends people hunting an
|
|
1934
|
+
* attacker when the fix is one env var.
|
|
1935
|
+
*/
|
|
1936
|
+
'no_secret_configured'
|
|
1937
|
+
/**
|
|
1938
|
+
* A caller-supplied option was not usable — a non-finite tolerance, a
|
|
1939
|
+
* negative window, a non-finite clock.
|
|
1940
|
+
*
|
|
1941
|
+
* Separate from {@link timestamp_out_of_tolerance} deliberately: that one
|
|
1942
|
+
* means "this delivery's clock disagrees with yours", and reporting a typo in
|
|
1943
|
+
* your own configuration under it sends people hunting clock skew on a
|
|
1944
|
+
* healthy box. Same reasoning as {@link no_secret_configured}.
|
|
1945
|
+
*/
|
|
1946
|
+
| 'invalid_option'
|
|
1947
|
+
/** No `X-Vairified-Signature` header at all. */
|
|
1948
|
+
| 'missing_signature'
|
|
1949
|
+
/** Present but unparseable, or missing its `t` / `v1` parts. */
|
|
1950
|
+
| 'malformed_signature'
|
|
1951
|
+
/** Outside the tolerance window, in either direction. */
|
|
1952
|
+
| 'timestamp_out_of_tolerance'
|
|
1953
|
+
/** Parsed fine; no supplied secret produces this digest. */
|
|
1954
|
+
| 'signature_mismatch'
|
|
1955
|
+
/** Verified, but the body is not JSON, or is not a webhook envelope. */
|
|
1956
|
+
| 'malformed_body';
|
|
1957
|
+
/**
|
|
1958
|
+
* Thrown when a webhook delivery cannot be trusted.
|
|
1959
|
+
*
|
|
1960
|
+
* :rotating_light: **The message never contains the signing secret, the
|
|
1961
|
+
* received digest, or the computed one.** Writing either digest into an error
|
|
1962
|
+
* puts a valid HMAC of the partner's own payload into their application logs,
|
|
1963
|
+
* where it is an oracle for anyone who can read them. Use {@link reason} to
|
|
1964
|
+
* branch; there is deliberately nothing finer-grained to log.
|
|
1965
|
+
*
|
|
1966
|
+
* @category Errors
|
|
1967
|
+
*/
|
|
1968
|
+
declare class WebhookSignatureError extends VairifiedError {
|
|
1969
|
+
readonly reason: WebhookRejectionReason;
|
|
1970
|
+
constructor(reason: WebhookRejectionReason, message: string);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
/**
|
|
1974
|
+
* Webhook signature verification.
|
|
1975
|
+
*
|
|
1976
|
+
* Standalone on purpose — a webhook receiver is an inbound HTTP handler. It
|
|
1977
|
+
* usually holds no API key, may never call the Partner API, and should not
|
|
1978
|
+
* have to construct a client just to check a signature.
|
|
1979
|
+
*
|
|
1980
|
+
* @module
|
|
1981
|
+
*/
|
|
1982
|
+
|
|
1983
|
+
/**
|
|
1984
|
+
* How far apart the delivery's timestamp and your clock may be, in seconds.
|
|
1985
|
+
*
|
|
1986
|
+
* Five minutes, matching the scheme this signature format follows. The window
|
|
1987
|
+
* is what stops a captured delivery being replayed indefinitely — without it a
|
|
1988
|
+
* valid signature stays valid forever.
|
|
1989
|
+
*/
|
|
1990
|
+
declare const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
1991
|
+
/** Options for {@link verifyWebhook}. */
|
|
1992
|
+
interface VerifyWebhookOptions {
|
|
1993
|
+
/**
|
|
1994
|
+
* Clock-skew allowance in seconds. Applied in **both** directions: a
|
|
1995
|
+
* delivery dated too far in the future is refused exactly as a stale one is.
|
|
1996
|
+
* A one-sided check would accept a forged future timestamp forever, which is
|
|
1997
|
+
* the replay hole the window exists to close.
|
|
1998
|
+
*
|
|
1999
|
+
* @defaultValue {@link DEFAULT_TOLERANCE_SECONDS}
|
|
2000
|
+
*/
|
|
2001
|
+
toleranceSeconds?: number;
|
|
2002
|
+
/**
|
|
2003
|
+
* Current time in **seconds** since the epoch. Injectable for tests; you
|
|
2004
|
+
* should not need it in production.
|
|
2005
|
+
*/
|
|
2006
|
+
nowSeconds?: number;
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Verify a webhook delivery and return it typed.
|
|
2010
|
+
*
|
|
2011
|
+
* @param rawBody - The **exact bytes** of the request body. This is the part
|
|
2012
|
+
* people get wrong: the signature covers what was sent, so a body that has
|
|
2013
|
+
* been parsed and re-serialised (`express.json()`, FastAPI's parsed body)
|
|
2014
|
+
* produces different bytes and every signature fails. Capture the raw body.
|
|
2015
|
+
* @param signatureHeader - The `X-Vairified-Signature` header value.
|
|
2016
|
+
* @param secret - Your webhook signing secret, or **several**. Pass both the
|
|
2017
|
+
* old and the new around a rotation: deliveries already queued were signed
|
|
2018
|
+
* with the old secret and keep arriving for hours afterwards, so a verifier
|
|
2019
|
+
* that knows only the new one discards them.
|
|
2020
|
+
* @param options - See {@link VerifyWebhookOptions}.
|
|
2021
|
+
* @returns The verified event. Narrow on `event` to reach the typed members.
|
|
2022
|
+
* @throws {@link WebhookSignatureError} for every refusal; read `reason`.
|
|
2023
|
+
*
|
|
2024
|
+
* @remarks
|
|
2025
|
+
* :rotating_light: **Deduplicate on the BODY's `eventId`, never on the
|
|
2026
|
+
* `X-Vairified-Event-Id` header.** Delivery is at-least-once, so a retry can
|
|
2027
|
+
* present the same event twice — but that header is **outside the signature**.
|
|
2028
|
+
* An attacker who captures one delivery can replay it inside the tolerance
|
|
2029
|
+
* window with a fresh header value, and header-based deduplication will let it
|
|
2030
|
+
* through every time. The body's `eventId` is covered by the HMAC; the header
|
|
2031
|
+
* is a convenience for routing, not an identity you can trust.
|
|
2032
|
+
*
|
|
2033
|
+
* Replay inside the window is otherwise unprevented by design: the timestamp
|
|
2034
|
+
* tolerance is the only replay control this function applies, so your own
|
|
2035
|
+
* deduplication is what stops a captured delivery being applied twice.
|
|
2036
|
+
*
|
|
2037
|
+
* :rotating_light: **`await` it.** It is async because it uses Web Crypto, which
|
|
2038
|
+
* has no synchronous HMAC. Forgetting the `await` on a *valid* signature hands
|
|
2039
|
+
* your code a Promise whose `.data` is `undefined` — an entitlement check then
|
|
2040
|
+
* reads falsy and denies a member who paid, silently. On an *invalid* one it is
|
|
2041
|
+
* an unhandled rejection, which on Node >=24 exits the process. This function
|
|
2042
|
+
* never returns a boolean, precisely so that mistake cannot be quiet.
|
|
2043
|
+
*
|
|
2044
|
+
* @example
|
|
2045
|
+
* ```ts
|
|
2046
|
+
* import { verifyWebhook, isMemberStatusEvent, WebhookSignatureError } from 'vairified';
|
|
2047
|
+
*
|
|
2048
|
+
* app.post('/hooks/vair', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
2049
|
+
* try {
|
|
2050
|
+
* const event = await verifyWebhook(
|
|
2051
|
+
* req.body, // a Buffer — the raw bytes
|
|
2052
|
+
* req.header('X-Vairified-Signature') ?? '',
|
|
2053
|
+
* [process.env.VAIR_WEBHOOK_SECRET, process.env.VAIR_WEBHOOK_SECRET_PREVIOUS],
|
|
2054
|
+
* );
|
|
2055
|
+
* // Narrow with the exported guards — `event.event === '…'` cannot narrow
|
|
2056
|
+
* // while the unknown-event fallback is part of the union.
|
|
2057
|
+
* if (isMemberStatusEvent(event)) {
|
|
2058
|
+
* console.log(event.data.memberId, event.data.isVairPlus);
|
|
2059
|
+
* }
|
|
2060
|
+
* res.sendStatus(204);
|
|
2061
|
+
* } catch (err) {
|
|
2062
|
+
* if (err instanceof WebhookSignatureError) return res.sendStatus(400);
|
|
2063
|
+
* throw err;
|
|
2064
|
+
* }
|
|
2065
|
+
* });
|
|
2066
|
+
* ```
|
|
2067
|
+
*
|
|
2068
|
+
* @category Webhooks
|
|
2069
|
+
*/
|
|
2070
|
+
declare function verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string | undefined | null | readonly (string | undefined | null)[], options?: VerifyWebhookOptions): Promise<VerifiedWebhookEvent>;
|
|
2071
|
+
/**
|
|
2072
|
+
* Narrow a verified event to `member.status`.
|
|
2073
|
+
*
|
|
2074
|
+
* @example
|
|
2075
|
+
* ```ts
|
|
2076
|
+
* const event = await verifyWebhook(rawBody, header, secret);
|
|
2077
|
+
* if (isMemberStatusEvent(event)) {
|
|
2078
|
+
* // `event.data` is fully typed here.
|
|
2079
|
+
* if (!event.data.isVairPlus) denyEntry(event.data.memberId);
|
|
2080
|
+
* }
|
|
2081
|
+
* ```
|
|
2082
|
+
*
|
|
2083
|
+
* @category Webhooks
|
|
2084
|
+
*/
|
|
2085
|
+
declare function isMemberStatusEvent(event: VerifiedWebhookEvent): event is MemberStatusEventWire;
|
|
2086
|
+
/**
|
|
2087
|
+
* Narrow a verified event to `connection.revoked`.
|
|
2088
|
+
*
|
|
2089
|
+
* :warning: `data.reason` is an open set — `player_deleted` and
|
|
2090
|
+
* `player_disconnected` today, more later. Never branch on it exhaustively.
|
|
2091
|
+
*
|
|
2092
|
+
* @category Webhooks
|
|
2093
|
+
*/
|
|
2094
|
+
declare function isConnectionRevokedEvent(event: VerifiedWebhookEvent): event is ConnectionRevokedEventWire;
|
|
2095
|
+
/**
|
|
2096
|
+
* Narrow a verified event to `rating.updated` — the event that makes up
|
|
2097
|
+
* almost all real traffic.
|
|
2098
|
+
*
|
|
2099
|
+
* :rotating_light: **`data` is NOT the shape {@link MembersResource.ratingUpdates}
|
|
2100
|
+
* returns when polling.** The webhook has carried a full multi-sport snapshot
|
|
2101
|
+
* since Vairified#899 and the two have not matched since — there is no
|
|
2102
|
+
* `previousRating`, `newRating` or `ratingSplits` on it. Reusing a polling
|
|
2103
|
+
* handler here reads fields that will never be present. See
|
|
2104
|
+
* {@link RatingUpdatedEventDataWire}.
|
|
2105
|
+
*
|
|
2106
|
+
* @category Webhooks
|
|
2107
|
+
*/
|
|
2108
|
+
declare function isRatingUpdatedEvent(event: VerifiedWebhookEvent): event is RatingUpdatedEventWire;
|
|
2109
|
+
/**
|
|
2110
|
+
* Narrow a verified event to `event.created`.
|
|
2111
|
+
*
|
|
2112
|
+
* :warning: `data.eventId` is a **number** and is not the envelope's string
|
|
2113
|
+
* `eventId`. Deduplicate on the envelope's; refer to the event by `data`'s.
|
|
2114
|
+
*
|
|
2115
|
+
* @category Webhooks
|
|
2116
|
+
*/
|
|
2117
|
+
declare function isEventCreatedEvent(event: VerifiedWebhookEvent): event is EventCreatedEventWire;
|
|
2118
|
+
/**
|
|
2119
|
+
* Compare two `sequence` values as integers.
|
|
2120
|
+
*
|
|
2121
|
+
* @returns negative if `a` is older, `0` if equal, positive if `a` is newer.
|
|
2122
|
+
* @category Webhooks
|
|
2123
|
+
*/
|
|
2124
|
+
declare function compareSequence(a: string, b: string): number;
|
|
2125
|
+
/**
|
|
2126
|
+
* Whether an incoming `sequence` is newer than the last one you applied **for
|
|
2127
|
+
* that same member**.
|
|
2128
|
+
*
|
|
2129
|
+
* Use it to discard stale deliveries. `rating.updated` carries a full snapshot
|
|
2130
|
+
* rather than a diff, so applying an older one last leaves you holding a rating
|
|
2131
|
+
* the member no longer has.
|
|
2132
|
+
*
|
|
2133
|
+
* ```ts
|
|
2134
|
+
* if (isRatingUpdatedEvent(event)) {
|
|
2135
|
+
* const last = await store.get(event.data.memberId);
|
|
2136
|
+
* if (last && !isNewerSequence(event.data.sequence, last)) return; // stale
|
|
2137
|
+
* await store.put(event.data.memberId, event.data.sequence);
|
|
2138
|
+
* }
|
|
2139
|
+
* ```
|
|
2140
|
+
*
|
|
2141
|
+
* @param incoming - the delivery's `sequence`.
|
|
2142
|
+
* @param lastApplied - the highest you have applied for that member, or
|
|
2143
|
+
* `null`/`undefined` if you have applied none — in which case this is `true`.
|
|
2144
|
+
* @category Webhooks
|
|
2145
|
+
*/
|
|
2146
|
+
declare function isNewerSequence(incoming: string, lastApplied?: string | null): boolean;
|
|
2147
|
+
/**
|
|
2148
|
+
* The key to deduplicate a delivery on.
|
|
2149
|
+
*
|
|
2150
|
+
* Delivery is at-least-once, so a retry can present the same event twice. This
|
|
2151
|
+
* returns the id **from the signed body**.
|
|
2152
|
+
*
|
|
2153
|
+
* :rotating_light: Calling this is the point. The `X-Vairified-Event-Id` header
|
|
2154
|
+
* carries the same value and is **not covered by the signature**, so an attacker
|
|
2155
|
+
* replaying a captured delivery inside the tolerance window can change it freely
|
|
2156
|
+
* and header-based deduplication lets it through every time.
|
|
2157
|
+
*
|
|
2158
|
+
* @category Webhooks
|
|
2159
|
+
*/
|
|
2160
|
+
declare function dedupeKey(event: VerifiedWebhookEvent): string;
|
|
1682
2161
|
|
|
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 };
|
|
2162
|
+
export { type ApiErrorResponse, AuthenticationError, type AuthorizationResponse, type ConnectionRevokedEventDataWire, type ConnectionRevokedEventWire, DEFAULT_SCOPES, DEFAULT_TOLERANCE_SECONDS, ENVIRONMENTS, type EventCreatedClubWire, type EventCreatedEventDataWire, type EventCreatedEventWire, type GameInput, type Gender, type LeaderboardOptions, LeaderboardResource, type MatchBatch, MatchBatchResult, type MatchBatchResultWire, type MatchInput, MatchesResource, Member, MemberEmailMatch, MemberSportMap, type MemberStatusEventDataWire, type MemberStatusEventSportWire, type MemberStatusEventWire, type MemberStatusWire, MembersByEmailResult, type MembersByEmailResultWire, MembersResource, NotFoundError, type OAuthConfig, OAuthError, OAuthResource, type OAuthScope, type OpenEnum, type PartnerMemberEmailMatchWire, type PartnerMemberWire, type PartnerRatingUpdateWire, type PlayerRankOptions, RateLimitError, type RatingSplitWire, RatingUpdate, type RatingUpdatedEventDataWire, type RatingUpdatedEventWire, type RatingUpdatedSportWire, SCOPES, type SearchFilters, SportRating, type SportRatingWire, type TokenResponse, TournamentImportResult, type TournamentImportResultWire, type UnknownWebhookEventWire, Vairified, type VairifiedEnvironment, VairifiedError, type VairifiedOptions, ValidationError, type VerifiedWebhookEvent, type VerifyWebhookOptions, WebhookDeliveriesResult, type WebhookDeliveriesResultWire, WebhookDelivery, type WebhookDeliveryWire, type WebhookEventEnvelopeWire, type WebhookRejectionReason, WebhookSignatureError, WebhooksResource, compareSequence, dedupeKey, describeScope, describeScopes, generateState, getAuthorizationUrl, isConnectionRevokedEvent, isEventCreatedEvent, isMemberStatusEvent, isNewerSequence, isRatingUpdatedEvent, validateScope, verifyWebhook };
|