@pickleball/server-sdk 0.1.1 → 0.3.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/CHANGELOG.md +30 -0
- package/README.md +22 -0
- package/dist/{chunk-HBTNLURN.js → chunk-UPS4VFER.js} +188 -21
- package/dist/index.cjs +189 -21
- package/dist/index.d.cts +168 -3
- package/dist/index.d.ts +168 -3
- package/dist/index.js +3 -1
- package/dist/proxy.cjs +321 -23
- package/dist/proxy.d.cts +11 -1
- package/dist/proxy.d.ts +11 -1
- package/dist/proxy.js +133 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @pickleball/server-sdk
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0d74c20: Camera grant (ADR-0002) cho điện thoại của user đối tác: namespace mới
|
|
8
|
+
`liveSessions` trên `createPickleballLiveClient` — `createCameraSession`
|
|
9
|
+
(`POST /api/v1/live-sessions {cameraGrant:true}`, idempotent theo matchRef),
|
|
10
|
+
`cameraGrant` (cấp lại grant, thu hồi grant cũ), `end`, `get`, `getRecordings`
|
|
11
|
+
đi đường REST v1 với `x-api-key`, không cần `appId` (option này thành tuỳ chọn).
|
|
12
|
+
Lỗi REST v1 `{error, code, ...extra}` thành `PickleballPartnerApiError`
|
|
13
|
+
(`partnerCode`, `details`). Contract `CameraGrant` (superset của
|
|
14
|
+
`SdkSessionGrant`). Các thao tác `/api/v2/sdk/*` và subpath `./proxy` được đánh
|
|
15
|
+
dấu deprecated, giữ nguyên hành vi.
|
|
16
|
+
|
|
17
|
+
## 0.2.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- Device registry endpoints for the partner proxy: `devices.register` and
|
|
22
|
+
`devices.heartbeat` on `createPickleballLiveClient`, plus the proxy handlers
|
|
23
|
+
`handleDeviceRegister` / `handleDeviceHeartbeat` (routes `/devices/register`
|
|
24
|
+
and `/devices/heartbeat`). The heartbeat response carries a one-time control
|
|
25
|
+
session grant so an operator dashboard can remote-start a livestream on a
|
|
26
|
+
device that is open but idle.
|
|
27
|
+
- New authorization operations `device_register` and `device_heartbeat`.
|
|
28
|
+
**Production authz gateways must allow these two operation names**; the
|
|
29
|
+
staging stub already allows everything.
|
|
30
|
+
- Version aligned with `@pickleball/expo-sdk@0.2.0` (both packages share a
|
|
31
|
+
stable dist-tag promotion).
|
|
32
|
+
|
|
3
33
|
## 0.1.1
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -56,6 +56,28 @@ Never expose this package's API key to a native/web bundle or log it. Put the
|
|
|
56
56
|
client in a server route, Worker, serverless function, or other trusted runtime.
|
|
57
57
|
The SDK never logs the API key, participant token, or webhook secret.
|
|
58
58
|
|
|
59
|
+
## Camera grant — điện thoại của user đối tác (ADR-0002)
|
|
60
|
+
|
|
61
|
+
Backend đối tác tạo phiên theo `matchRef` và nhận **camera grant**; app đối tác
|
|
62
|
+
(`@pickleball/expo-sdk` chế độ grant) dùng `cameraGrant.telemetry.token` làm
|
|
63
|
+
Bearer để nói thẳng với Picklive — không proxy, không cần `appId`:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const client = createPickleballLiveClient({ baseUrl, apiKey });
|
|
67
|
+
const { id, watchUrl, cameraGrant } = await client.liveSessions.createCameraSession({
|
|
68
|
+
matchRef: match.id,
|
|
69
|
+
title: match.title,
|
|
70
|
+
quality: 720,
|
|
71
|
+
// courtRef: "san-3", // tuỳ chọn: đậu vào sân giải để dùng match-start/match-end
|
|
72
|
+
});
|
|
73
|
+
// Trả `cameraGrant` NGUYÊN TRẠNG cho app qua auth riêng của bạn. Không log grant.
|
|
74
|
+
// App bị xoá/cài lại hoặc token hết hạn: client.liveSessions.cameraGrant(id) cấp lại
|
|
75
|
+
// (grant cũ bị thu hồi — máy cũ dừng, không tranh sóng với máy mới).
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Lỗi REST v1 ném `PickleballPartnerApiError` với `partnerCode` (VD
|
|
79
|
+
`COURT_HAS_DEVICE`, `COURT_BUSY`, `CAPACITY`, `SESSION_EXPIRED`) và `details`.
|
|
80
|
+
|
|
59
81
|
## Webhook verification
|
|
60
82
|
|
|
61
83
|
Verify the exact raw request bytes before parsing. Delivery is at-least-once:
|
|
@@ -19,6 +19,15 @@ var PickleballApiError = class extends PickleballLiveError {
|
|
|
19
19
|
this.name = "PickleballApiError";
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
|
+
var PickleballPartnerApiError = class extends PickleballLiveError {
|
|
23
|
+
constructor(message, partnerCode, status, details = {}) {
|
|
24
|
+
super(message, "PARTNER_API_ERROR");
|
|
25
|
+
this.partnerCode = partnerCode;
|
|
26
|
+
this.status = status;
|
|
27
|
+
this.details = details;
|
|
28
|
+
this.name = "PickleballPartnerApiError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
22
31
|
var PickleballHttpError = class extends PickleballLiveError {
|
|
23
32
|
constructor(status) {
|
|
24
33
|
super(`Request failed with HTTP ${status}`, "HTTP_ERROR");
|
|
@@ -247,6 +256,49 @@ function validateGrant(value) {
|
|
|
247
256
|
config: validateRemoteConfig(data.config)
|
|
248
257
|
};
|
|
249
258
|
}
|
|
259
|
+
function validateCameraGrant(value) {
|
|
260
|
+
const data = record(value);
|
|
261
|
+
const base = validateGrant(data);
|
|
262
|
+
if (!/^rtmps?:\/\//i.test(base.serverUrl)) invalid();
|
|
263
|
+
if (data.status !== "scheduled" && data.status !== "live") invalid();
|
|
264
|
+
return {
|
|
265
|
+
...base,
|
|
266
|
+
status: data.status,
|
|
267
|
+
matchRef: nullableString(data.matchRef),
|
|
268
|
+
courtRef: nullableString(data.courtRef),
|
|
269
|
+
watchUrl: nullableString(data.watchUrl),
|
|
270
|
+
convexUrl: nullableString(data.convexUrl),
|
|
271
|
+
issuedAt: finite(data.issuedAt),
|
|
272
|
+
expiresAt: finite(data.expiresAt)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function validateCameraSessionCreated(value) {
|
|
276
|
+
const data = record(value);
|
|
277
|
+
if (data.status !== "scheduled" && data.status !== "live" && data.status !== "ended") {
|
|
278
|
+
invalid();
|
|
279
|
+
}
|
|
280
|
+
if (data.visibility !== "public" && data.visibility !== "private") invalid();
|
|
281
|
+
if (typeof data.reused !== "boolean") invalid();
|
|
282
|
+
return {
|
|
283
|
+
id: nonblank(data.id),
|
|
284
|
+
status: data.status,
|
|
285
|
+
visibility: data.visibility,
|
|
286
|
+
quality: finite(data.quality),
|
|
287
|
+
deliveryMode: nonblank(data.deliveryMode),
|
|
288
|
+
matchRef: nullableString(data.matchRef),
|
|
289
|
+
courtRef: nullableString(data.courtRef),
|
|
290
|
+
playbackUrl: nullableString(data.playbackUrl),
|
|
291
|
+
whepUrl: nullableString(data.whepUrl),
|
|
292
|
+
watchUrl: nullableString(data.watchUrl),
|
|
293
|
+
reused: data.reused,
|
|
294
|
+
cameraGrant: validateCameraGrant(data.cameraGrant)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function validateLegacyEnd(value) {
|
|
298
|
+
const data = record(value);
|
|
299
|
+
if (data.ok !== true || data.status !== "ended") invalid();
|
|
300
|
+
return { status: "ended" };
|
|
301
|
+
}
|
|
250
302
|
function validateRefresh(value) {
|
|
251
303
|
const data = record(value);
|
|
252
304
|
return {
|
|
@@ -284,6 +336,7 @@ function validateSession(value) {
|
|
|
284
336
|
matchRef: nullableString(data.matchRef),
|
|
285
337
|
playbackUrl: nullableString(data.playbackUrl),
|
|
286
338
|
watchUrl: nullableString(data.watchUrl),
|
|
339
|
+
whepUrl: nullableString(data.whepUrl),
|
|
287
340
|
scheduledAt: nullableNumber(data.scheduledAt),
|
|
288
341
|
startedAt: nullableNumber(data.startedAt),
|
|
289
342
|
endedAt: nullableNumber(data.endedAt),
|
|
@@ -314,6 +367,29 @@ function validateRecordings(value, sessionId) {
|
|
|
314
367
|
if (!Array.isArray(value)) invalid();
|
|
315
368
|
return value.map((item) => validateRecording(item, sessionId));
|
|
316
369
|
}
|
|
370
|
+
function validateDeviceRegister(value) {
|
|
371
|
+
const data = record(value);
|
|
372
|
+
const heartbeatIntervalMs = finite(data.heartbeatIntervalMs);
|
|
373
|
+
if (heartbeatIntervalMs <= 0) invalid();
|
|
374
|
+
return {
|
|
375
|
+
deviceId: nonblank(data.deviceId),
|
|
376
|
+
heartbeatIntervalMs
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function validateDeviceHeartbeat(value) {
|
|
380
|
+
const data = record(value);
|
|
381
|
+
if (data.controlSession === null || data.controlSession === void 0) {
|
|
382
|
+
return { controlSession: null };
|
|
383
|
+
}
|
|
384
|
+
const grant = record(data.controlSession);
|
|
385
|
+
return {
|
|
386
|
+
controlSession: {
|
|
387
|
+
id: nonblank(grant.id),
|
|
388
|
+
token: nonblank(grant.token),
|
|
389
|
+
convexUrl: nonblank(grant.convexUrl)
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
317
393
|
|
|
318
394
|
// src/contracts.ts
|
|
319
395
|
var SDK_TELEMETRY_EVENT_NAMES = [
|
|
@@ -459,7 +535,23 @@ async function readBoundedResponseText(response) {
|
|
|
459
535
|
}
|
|
460
536
|
return new TextDecoder().decode(bytes);
|
|
461
537
|
}
|
|
462
|
-
|
|
538
|
+
function parsePartnerError(payload, status, apiKey) {
|
|
539
|
+
if (!isRecord(payload) || !("error" in payload)) {
|
|
540
|
+
return new PickleballInvalidResponseError();
|
|
541
|
+
}
|
|
542
|
+
if (isRecord(payload.error)) {
|
|
543
|
+
return parseSdkError(payload, status, apiKey);
|
|
544
|
+
}
|
|
545
|
+
if (typeof payload.error !== "string") return new PickleballInvalidResponseError();
|
|
546
|
+
const { error, code, ...details } = payload;
|
|
547
|
+
const safeMessage = apiKey === "" ? error : error.split(apiKey).join("[REDACTED]");
|
|
548
|
+
const partnerCode = typeof code === "string" && code.trim() !== "" ? code : `HTTP_${status}`;
|
|
549
|
+
return new PickleballPartnerApiError(safeMessage, partnerCode, status, details);
|
|
550
|
+
}
|
|
551
|
+
async function decodeResponse(response, apiKey, validate, options = {
|
|
552
|
+
errorStyle: "sdk",
|
|
553
|
+
envelope: "data"
|
|
554
|
+
}) {
|
|
463
555
|
if (response.status >= 300 && response.status < 400) {
|
|
464
556
|
await response.body?.cancel().catch(() => void 0);
|
|
465
557
|
throw new PickleballHttpError(response.status);
|
|
@@ -471,7 +563,10 @@ async function decodeResponse(response, apiKey, validate) {
|
|
|
471
563
|
} catch {
|
|
472
564
|
throw new PickleballInvalidResponseError();
|
|
473
565
|
}
|
|
474
|
-
if (!response.ok)
|
|
566
|
+
if (!response.ok) {
|
|
567
|
+
throw options.errorStyle === "v1" ? parsePartnerError(payload, response.status, apiKey) : parseSdkError(payload, response.status, apiKey);
|
|
568
|
+
}
|
|
569
|
+
if (options.envelope === "raw") return validate(payload);
|
|
475
570
|
if (!isRecord(payload) || !("data" in payload)) {
|
|
476
571
|
throw new PickleballInvalidResponseError();
|
|
477
572
|
}
|
|
@@ -479,7 +574,15 @@ async function decodeResponse(response, apiKey, validate) {
|
|
|
479
574
|
}
|
|
480
575
|
function createPickleballLiveClient(options) {
|
|
481
576
|
const baseUrl = validatedBaseUrl(options.baseUrl);
|
|
482
|
-
const
|
|
577
|
+
const configuredAppId = options.appId === void 0 ? null : requiredNonblank(options.appId, "appId");
|
|
578
|
+
const requireAppId = () => {
|
|
579
|
+
if (configuredAppId === null) {
|
|
580
|
+
throw new PickleballConfigurationError(
|
|
581
|
+
"appId is required for /api/v2/sdk operations (apps/sessions/devices)"
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
return configuredAppId;
|
|
585
|
+
};
|
|
483
586
|
const apiKey = requiredNonblank(options.apiKey, "apiKey");
|
|
484
587
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
485
588
|
if (typeof fetchImplementation !== "function") {
|
|
@@ -492,7 +595,9 @@ function createPickleballLiveClient(options) {
|
|
|
492
595
|
path,
|
|
493
596
|
body,
|
|
494
597
|
retryPolicy,
|
|
495
|
-
validate
|
|
598
|
+
validate,
|
|
599
|
+
errorStyle = "sdk",
|
|
600
|
+
envelope = "data"
|
|
496
601
|
}) {
|
|
497
602
|
const retryLimit = retryPolicy === "transient" ? maxRetries : 0;
|
|
498
603
|
for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
|
|
@@ -513,14 +618,14 @@ function createPickleballLiveClient(options) {
|
|
|
513
618
|
} : {
|
|
514
619
|
accept: "application/json",
|
|
515
620
|
"x-api-key": apiKey,
|
|
516
|
-
"x-pickleball-app-id":
|
|
621
|
+
...configuredAppId === null ? {} : { "x-pickleball-app-id": configuredAppId }
|
|
517
622
|
},
|
|
518
623
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
519
624
|
signal: controller.signal,
|
|
520
625
|
redirect: "error"
|
|
521
626
|
});
|
|
522
627
|
try {
|
|
523
|
-
return await decodeResponse(response, apiKey, validate);
|
|
628
|
+
return await decodeResponse(response, apiKey, validate, { errorStyle, envelope });
|
|
524
629
|
} catch (error) {
|
|
525
630
|
if (isTransientStatus(response.status) && attempt < retryLimit && isRetryableResponseError(error)) {
|
|
526
631
|
clearTimeout(timer);
|
|
@@ -532,7 +637,7 @@ function createPickleballLiveClient(options) {
|
|
|
532
637
|
throw error;
|
|
533
638
|
}
|
|
534
639
|
} catch (error) {
|
|
535
|
-
if (error instanceof PickleballApiError || error instanceof PickleballHttpError || error instanceof PickleballInvalidResponseError) {
|
|
640
|
+
if (error instanceof PickleballApiError || error instanceof PickleballPartnerApiError || error instanceof PickleballHttpError || error instanceof PickleballInvalidResponseError) {
|
|
536
641
|
throw error;
|
|
537
642
|
}
|
|
538
643
|
if (attempt < retryLimit) {
|
|
@@ -551,54 +656,115 @@ function createPickleballLiveClient(options) {
|
|
|
551
656
|
const post = (path, body, retryPolicy, validate) => request({ method: "POST", path, body, retryPolicy, validate });
|
|
552
657
|
const get = (path, validate) => request({ method: "GET", path, retryPolicy: "transient", validate });
|
|
553
658
|
const sessionPath = (sessionId) => `/api/v2/sdk/sessions/${encodeURIComponent(sessionId)}`;
|
|
659
|
+
const v1SessionPath = (sessionId) => `/api/v1/live-sessions/${encodeURIComponent(sessionId)}`;
|
|
660
|
+
const postV1 = (path, body, retryPolicy, validate, envelope = "data") => request({ method: "POST", path, body, retryPolicy, validate, errorStyle: "v1", envelope });
|
|
661
|
+
const getV1 = (path, validate) => request({ method: "GET", path, retryPolicy: "transient", validate, errorStyle: "v1" });
|
|
554
662
|
return {
|
|
555
663
|
apps: {
|
|
556
|
-
bootstrap: (input) => post(
|
|
664
|
+
bootstrap: async (input) => post(
|
|
557
665
|
"/api/v2/sdk/bootstrap",
|
|
558
|
-
{ ...input, appId },
|
|
666
|
+
{ ...input, appId: requireAppId() },
|
|
559
667
|
"transient",
|
|
560
668
|
validateBootstrap
|
|
561
669
|
)
|
|
562
670
|
},
|
|
563
671
|
sessions: {
|
|
564
|
-
start: (input) => post(
|
|
672
|
+
start: async (input) => post(
|
|
565
673
|
"/api/v2/sdk/sessions",
|
|
566
|
-
{ ...input, appId },
|
|
674
|
+
{ ...input, appId: requireAppId() },
|
|
567
675
|
"transient",
|
|
568
676
|
validateGrant
|
|
569
677
|
),
|
|
570
|
-
refresh: (sessionId, input) => post(
|
|
678
|
+
refresh: async (sessionId, input) => post(
|
|
571
679
|
`${sessionPath(sessionId)}/refresh`,
|
|
572
|
-
{ ...input, appId },
|
|
680
|
+
{ ...input, appId: requireAppId() },
|
|
573
681
|
"never",
|
|
574
682
|
validateRefresh
|
|
575
683
|
),
|
|
576
684
|
end: async (input) => {
|
|
577
685
|
await post(
|
|
578
686
|
`${sessionPath(input.sessionId)}/end`,
|
|
579
|
-
{ ...input, appId },
|
|
687
|
+
{ ...input, appId: requireAppId() },
|
|
580
688
|
"transient",
|
|
581
689
|
(value) => validateEnd(value, input.sessionId)
|
|
582
690
|
);
|
|
583
691
|
},
|
|
584
|
-
publish: (sessionId) => post(
|
|
692
|
+
publish: async (sessionId) => post(
|
|
585
693
|
`${sessionPath(sessionId)}/publish`,
|
|
586
|
-
{ sessionId, appId },
|
|
694
|
+
{ sessionId, appId: requireAppId() },
|
|
587
695
|
"transient",
|
|
588
696
|
(value) => validatePublishState(value, sessionId)
|
|
589
697
|
),
|
|
590
|
-
unpublish: (sessionId) => post(
|
|
698
|
+
unpublish: async (sessionId) => post(
|
|
591
699
|
`${sessionPath(sessionId)}/unpublish`,
|
|
592
|
-
{ sessionId, appId },
|
|
700
|
+
{ sessionId, appId: requireAppId() },
|
|
593
701
|
"transient",
|
|
594
702
|
(value) => validatePublishState(value, sessionId)
|
|
595
703
|
),
|
|
596
|
-
get: (sessionId) =>
|
|
597
|
-
|
|
598
|
-
|
|
704
|
+
get: async (sessionId) => {
|
|
705
|
+
requireAppId();
|
|
706
|
+
return get(sessionPath(sessionId), validateSession);
|
|
707
|
+
},
|
|
708
|
+
getRecordings: async (sessionId) => {
|
|
709
|
+
requireAppId();
|
|
710
|
+
return get(
|
|
711
|
+
`${sessionPath(sessionId)}/recordings`,
|
|
712
|
+
(value) => validateRecordings(value, sessionId)
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
liveSessions: {
|
|
717
|
+
createCameraSession: (input) => postV1(
|
|
718
|
+
"/api/v1/live-sessions",
|
|
719
|
+
{
|
|
720
|
+
title: input.title,
|
|
721
|
+
matchRef: input.matchRef,
|
|
722
|
+
cameraGrant: true,
|
|
723
|
+
deliveryMode: "device-rtmp",
|
|
724
|
+
...input.visibility === void 0 ? {} : { visibility: input.visibility },
|
|
725
|
+
...input.description === void 0 ? {} : { description: input.description },
|
|
726
|
+
...input.quality === void 0 ? {} : { quality: input.quality },
|
|
727
|
+
...input.courtRef === void 0 ? {} : { courtRef: input.courtRef }
|
|
728
|
+
},
|
|
729
|
+
// Idempotent theo (host, matchRef): gọi lại chỉ cấp grant mới, an toàn để retry.
|
|
730
|
+
"transient",
|
|
731
|
+
validateCameraSessionCreated
|
|
732
|
+
),
|
|
733
|
+
cameraGrant: (sessionId) => postV1(
|
|
734
|
+
`${v1SessionPath(sessionId)}/camera-grant`,
|
|
735
|
+
{},
|
|
736
|
+
"transient",
|
|
737
|
+
validateCameraGrant
|
|
738
|
+
),
|
|
739
|
+
end: (sessionId) => postV1(
|
|
740
|
+
`${v1SessionPath(sessionId)}/end`,
|
|
741
|
+
{},
|
|
742
|
+
"transient",
|
|
743
|
+
validateLegacyEnd,
|
|
744
|
+
"raw"
|
|
745
|
+
),
|
|
746
|
+
get: (sessionId) => getV1(v1SessionPath(sessionId), validateSession),
|
|
747
|
+
getRecordings: (sessionId) => getV1(
|
|
748
|
+
`${v1SessionPath(sessionId)}/recordings`,
|
|
599
749
|
(value) => validateRecordings(value, sessionId)
|
|
600
750
|
)
|
|
601
751
|
},
|
|
752
|
+
devices: {
|
|
753
|
+
register: async (input) => post(
|
|
754
|
+
"/api/v2/sdk/devices/register",
|
|
755
|
+
{ ...input, appId: requireAppId() },
|
|
756
|
+
"transient",
|
|
757
|
+
validateDeviceRegister
|
|
758
|
+
),
|
|
759
|
+
heartbeat: async (input) => post(
|
|
760
|
+
"/api/v2/sdk/devices/heartbeat",
|
|
761
|
+
{ ...input, appId: requireAppId() },
|
|
762
|
+
// Heartbeat có side effect trao token 1 lần — không retry để tránh
|
|
763
|
+
// nhận token đã bị đánh dấu delivered ở lần trước
|
|
764
|
+
"never",
|
|
765
|
+
validateDeviceHeartbeat
|
|
766
|
+
)
|
|
767
|
+
},
|
|
602
768
|
webhooks: { verify: verifyWebhook }
|
|
603
769
|
};
|
|
604
770
|
}
|
|
@@ -607,6 +773,7 @@ export {
|
|
|
607
773
|
PickleballLiveError,
|
|
608
774
|
PickleballConfigurationError,
|
|
609
775
|
PickleballApiError,
|
|
776
|
+
PickleballPartnerApiError,
|
|
610
777
|
PickleballHttpError,
|
|
611
778
|
PickleballInvalidResponseError,
|
|
612
779
|
PickleballTimeoutError,
|