vairified 0.6.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/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
  /**
@@ -453,6 +466,240 @@ interface AttributionResultWire {
453
466
  readonly outcome: AttributionOutcome;
454
467
  }[];
455
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;
456
703
 
457
704
  /**
458
705
  * {@link LeaderboardResource} — read-only leaderboard queries.
@@ -876,9 +1123,14 @@ declare class MembersByEmailResult {
876
1123
  /**
877
1124
  * A single rating change notification.
878
1125
  *
879
- * Returned by {@link MembersResource.ratingUpdates} (polling) and
880
- * delivered via webhook callbacks to partners that have registered a
881
- * webhook URL and have subscribers.
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.
882
1134
  *
883
1135
  * @category Members
884
1136
  */
@@ -1666,5 +1918,245 @@ declare class OAuthError extends VairifiedError {
1666
1918
  readonly errorCode?: string;
1667
1919
  constructor(message?: string, errorCode?: string, response?: unknown);
1668
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;
1669
2161
 
1670
- 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 };