@pickleball/server-sdk 0.2.0 → 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 +14 -0
- package/README.md +22 -0
- package/dist/{chunk-Y635I73O.js → chunk-UPS4VFER.js} +153 -25
- package/dist/index.cjs +154 -25
- package/dist/index.d.cts +124 -3
- package/dist/index.d.ts +124 -3
- package/dist/index.js +3 -1
- package/dist/proxy.cjs +155 -27
- package/dist/proxy.d.cts +7 -0
- package/dist/proxy.d.ts +7 -0
- package/dist/proxy.js +4 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
3
17
|
## 0.2.0
|
|
4
18
|
|
|
5
19
|
### Minor 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),
|
|
@@ -482,7 +535,23 @@ async function readBoundedResponseText(response) {
|
|
|
482
535
|
}
|
|
483
536
|
return new TextDecoder().decode(bytes);
|
|
484
537
|
}
|
|
485
|
-
|
|
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
|
+
}) {
|
|
486
555
|
if (response.status >= 300 && response.status < 400) {
|
|
487
556
|
await response.body?.cancel().catch(() => void 0);
|
|
488
557
|
throw new PickleballHttpError(response.status);
|
|
@@ -494,7 +563,10 @@ async function decodeResponse(response, apiKey, validate) {
|
|
|
494
563
|
} catch {
|
|
495
564
|
throw new PickleballInvalidResponseError();
|
|
496
565
|
}
|
|
497
|
-
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);
|
|
498
570
|
if (!isRecord(payload) || !("data" in payload)) {
|
|
499
571
|
throw new PickleballInvalidResponseError();
|
|
500
572
|
}
|
|
@@ -502,7 +574,15 @@ async function decodeResponse(response, apiKey, validate) {
|
|
|
502
574
|
}
|
|
503
575
|
function createPickleballLiveClient(options) {
|
|
504
576
|
const baseUrl = validatedBaseUrl(options.baseUrl);
|
|
505
|
-
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
|
+
};
|
|
506
586
|
const apiKey = requiredNonblank(options.apiKey, "apiKey");
|
|
507
587
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
508
588
|
if (typeof fetchImplementation !== "function") {
|
|
@@ -515,7 +595,9 @@ function createPickleballLiveClient(options) {
|
|
|
515
595
|
path,
|
|
516
596
|
body,
|
|
517
597
|
retryPolicy,
|
|
518
|
-
validate
|
|
598
|
+
validate,
|
|
599
|
+
errorStyle = "sdk",
|
|
600
|
+
envelope = "data"
|
|
519
601
|
}) {
|
|
520
602
|
const retryLimit = retryPolicy === "transient" ? maxRetries : 0;
|
|
521
603
|
for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
|
|
@@ -536,14 +618,14 @@ function createPickleballLiveClient(options) {
|
|
|
536
618
|
} : {
|
|
537
619
|
accept: "application/json",
|
|
538
620
|
"x-api-key": apiKey,
|
|
539
|
-
"x-pickleball-app-id":
|
|
621
|
+
...configuredAppId === null ? {} : { "x-pickleball-app-id": configuredAppId }
|
|
540
622
|
},
|
|
541
623
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
542
624
|
signal: controller.signal,
|
|
543
625
|
redirect: "error"
|
|
544
626
|
});
|
|
545
627
|
try {
|
|
546
|
-
return await decodeResponse(response, apiKey, validate);
|
|
628
|
+
return await decodeResponse(response, apiKey, validate, { errorStyle, envelope });
|
|
547
629
|
} catch (error) {
|
|
548
630
|
if (isTransientStatus(response.status) && attempt < retryLimit && isRetryableResponseError(error)) {
|
|
549
631
|
clearTimeout(timer);
|
|
@@ -555,7 +637,7 @@ function createPickleballLiveClient(options) {
|
|
|
555
637
|
throw error;
|
|
556
638
|
}
|
|
557
639
|
} catch (error) {
|
|
558
|
-
if (error instanceof PickleballApiError || error instanceof PickleballHttpError || error instanceof PickleballInvalidResponseError) {
|
|
640
|
+
if (error instanceof PickleballApiError || error instanceof PickleballPartnerApiError || error instanceof PickleballHttpError || error instanceof PickleballInvalidResponseError) {
|
|
559
641
|
throw error;
|
|
560
642
|
}
|
|
561
643
|
if (attempt < retryLimit) {
|
|
@@ -574,64 +656,109 @@ function createPickleballLiveClient(options) {
|
|
|
574
656
|
const post = (path, body, retryPolicy, validate) => request({ method: "POST", path, body, retryPolicy, validate });
|
|
575
657
|
const get = (path, validate) => request({ method: "GET", path, retryPolicy: "transient", validate });
|
|
576
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" });
|
|
577
662
|
return {
|
|
578
663
|
apps: {
|
|
579
|
-
bootstrap: (input) => post(
|
|
664
|
+
bootstrap: async (input) => post(
|
|
580
665
|
"/api/v2/sdk/bootstrap",
|
|
581
|
-
{ ...input, appId },
|
|
666
|
+
{ ...input, appId: requireAppId() },
|
|
582
667
|
"transient",
|
|
583
668
|
validateBootstrap
|
|
584
669
|
)
|
|
585
670
|
},
|
|
586
671
|
sessions: {
|
|
587
|
-
start: (input) => post(
|
|
672
|
+
start: async (input) => post(
|
|
588
673
|
"/api/v2/sdk/sessions",
|
|
589
|
-
{ ...input, appId },
|
|
674
|
+
{ ...input, appId: requireAppId() },
|
|
590
675
|
"transient",
|
|
591
676
|
validateGrant
|
|
592
677
|
),
|
|
593
|
-
refresh: (sessionId, input) => post(
|
|
678
|
+
refresh: async (sessionId, input) => post(
|
|
594
679
|
`${sessionPath(sessionId)}/refresh`,
|
|
595
|
-
{ ...input, appId },
|
|
680
|
+
{ ...input, appId: requireAppId() },
|
|
596
681
|
"never",
|
|
597
682
|
validateRefresh
|
|
598
683
|
),
|
|
599
684
|
end: async (input) => {
|
|
600
685
|
await post(
|
|
601
686
|
`${sessionPath(input.sessionId)}/end`,
|
|
602
|
-
{ ...input, appId },
|
|
687
|
+
{ ...input, appId: requireAppId() },
|
|
603
688
|
"transient",
|
|
604
689
|
(value) => validateEnd(value, input.sessionId)
|
|
605
690
|
);
|
|
606
691
|
},
|
|
607
|
-
publish: (sessionId) => post(
|
|
692
|
+
publish: async (sessionId) => post(
|
|
608
693
|
`${sessionPath(sessionId)}/publish`,
|
|
609
|
-
{ sessionId, appId },
|
|
694
|
+
{ sessionId, appId: requireAppId() },
|
|
610
695
|
"transient",
|
|
611
696
|
(value) => validatePublishState(value, sessionId)
|
|
612
697
|
),
|
|
613
|
-
unpublish: (sessionId) => post(
|
|
698
|
+
unpublish: async (sessionId) => post(
|
|
614
699
|
`${sessionPath(sessionId)}/unpublish`,
|
|
615
|
-
{ sessionId, appId },
|
|
700
|
+
{ sessionId, appId: requireAppId() },
|
|
616
701
|
"transient",
|
|
617
702
|
(value) => validatePublishState(value, sessionId)
|
|
618
703
|
),
|
|
619
|
-
get: (sessionId) =>
|
|
620
|
-
|
|
621
|
-
|
|
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`,
|
|
622
749
|
(value) => validateRecordings(value, sessionId)
|
|
623
750
|
)
|
|
624
751
|
},
|
|
625
752
|
devices: {
|
|
626
|
-
register: (input) => post(
|
|
753
|
+
register: async (input) => post(
|
|
627
754
|
"/api/v2/sdk/devices/register",
|
|
628
|
-
{ ...input, appId },
|
|
755
|
+
{ ...input, appId: requireAppId() },
|
|
629
756
|
"transient",
|
|
630
757
|
validateDeviceRegister
|
|
631
758
|
),
|
|
632
|
-
heartbeat: (input) => post(
|
|
759
|
+
heartbeat: async (input) => post(
|
|
633
760
|
"/api/v2/sdk/devices/heartbeat",
|
|
634
|
-
{ ...input, appId },
|
|
761
|
+
{ ...input, appId: requireAppId() },
|
|
635
762
|
// Heartbeat có side effect trao token 1 lần — không retry để tránh
|
|
636
763
|
// nhận token đã bị đánh dấu delivered ở lần trước
|
|
637
764
|
"never",
|
|
@@ -646,6 +773,7 @@ export {
|
|
|
646
773
|
PickleballLiveError,
|
|
647
774
|
PickleballConfigurationError,
|
|
648
775
|
PickleballApiError,
|
|
776
|
+
PickleballPartnerApiError,
|
|
649
777
|
PickleballHttpError,
|
|
650
778
|
PickleballInvalidResponseError,
|
|
651
779
|
PickleballTimeoutError,
|
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
PickleballInvalidResponseError: () => PickleballInvalidResponseError,
|
|
27
27
|
PickleballLiveError: () => PickleballLiveError,
|
|
28
28
|
PickleballNetworkError: () => PickleballNetworkError,
|
|
29
|
+
PickleballPartnerApiError: () => PickleballPartnerApiError,
|
|
29
30
|
PickleballTimeoutError: () => PickleballTimeoutError,
|
|
30
31
|
PickleballWebhookVerificationError: () => PickleballWebhookVerificationError,
|
|
31
32
|
SDK_TELEMETRY_EVENT_NAMES: () => SDK_TELEMETRY_EVENT_NAMES,
|
|
@@ -55,6 +56,15 @@ var PickleballApiError = class extends PickleballLiveError {
|
|
|
55
56
|
this.name = "PickleballApiError";
|
|
56
57
|
}
|
|
57
58
|
};
|
|
59
|
+
var PickleballPartnerApiError = class extends PickleballLiveError {
|
|
60
|
+
constructor(message, partnerCode, status, details = {}) {
|
|
61
|
+
super(message, "PARTNER_API_ERROR");
|
|
62
|
+
this.partnerCode = partnerCode;
|
|
63
|
+
this.status = status;
|
|
64
|
+
this.details = details;
|
|
65
|
+
this.name = "PickleballPartnerApiError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
58
68
|
var PickleballHttpError = class extends PickleballLiveError {
|
|
59
69
|
constructor(status) {
|
|
60
70
|
super(`Request failed with HTTP ${status}`, "HTTP_ERROR");
|
|
@@ -283,6 +293,49 @@ function validateGrant(value) {
|
|
|
283
293
|
config: validateRemoteConfig(data.config)
|
|
284
294
|
};
|
|
285
295
|
}
|
|
296
|
+
function validateCameraGrant(value) {
|
|
297
|
+
const data = record(value);
|
|
298
|
+
const base = validateGrant(data);
|
|
299
|
+
if (!/^rtmps?:\/\//i.test(base.serverUrl)) invalid();
|
|
300
|
+
if (data.status !== "scheduled" && data.status !== "live") invalid();
|
|
301
|
+
return {
|
|
302
|
+
...base,
|
|
303
|
+
status: data.status,
|
|
304
|
+
matchRef: nullableString(data.matchRef),
|
|
305
|
+
courtRef: nullableString(data.courtRef),
|
|
306
|
+
watchUrl: nullableString(data.watchUrl),
|
|
307
|
+
convexUrl: nullableString(data.convexUrl),
|
|
308
|
+
issuedAt: finite(data.issuedAt),
|
|
309
|
+
expiresAt: finite(data.expiresAt)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function validateCameraSessionCreated(value) {
|
|
313
|
+
const data = record(value);
|
|
314
|
+
if (data.status !== "scheduled" && data.status !== "live" && data.status !== "ended") {
|
|
315
|
+
invalid();
|
|
316
|
+
}
|
|
317
|
+
if (data.visibility !== "public" && data.visibility !== "private") invalid();
|
|
318
|
+
if (typeof data.reused !== "boolean") invalid();
|
|
319
|
+
return {
|
|
320
|
+
id: nonblank(data.id),
|
|
321
|
+
status: data.status,
|
|
322
|
+
visibility: data.visibility,
|
|
323
|
+
quality: finite(data.quality),
|
|
324
|
+
deliveryMode: nonblank(data.deliveryMode),
|
|
325
|
+
matchRef: nullableString(data.matchRef),
|
|
326
|
+
courtRef: nullableString(data.courtRef),
|
|
327
|
+
playbackUrl: nullableString(data.playbackUrl),
|
|
328
|
+
whepUrl: nullableString(data.whepUrl),
|
|
329
|
+
watchUrl: nullableString(data.watchUrl),
|
|
330
|
+
reused: data.reused,
|
|
331
|
+
cameraGrant: validateCameraGrant(data.cameraGrant)
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function validateLegacyEnd(value) {
|
|
335
|
+
const data = record(value);
|
|
336
|
+
if (data.ok !== true || data.status !== "ended") invalid();
|
|
337
|
+
return { status: "ended" };
|
|
338
|
+
}
|
|
286
339
|
function validateRefresh(value) {
|
|
287
340
|
const data = record(value);
|
|
288
341
|
return {
|
|
@@ -320,6 +373,7 @@ function validateSession(value) {
|
|
|
320
373
|
matchRef: nullableString(data.matchRef),
|
|
321
374
|
playbackUrl: nullableString(data.playbackUrl),
|
|
322
375
|
watchUrl: nullableString(data.watchUrl),
|
|
376
|
+
whepUrl: nullableString(data.whepUrl),
|
|
323
377
|
scheduledAt: nullableNumber(data.scheduledAt),
|
|
324
378
|
startedAt: nullableNumber(data.startedAt),
|
|
325
379
|
endedAt: nullableNumber(data.endedAt),
|
|
@@ -518,7 +572,23 @@ async function readBoundedResponseText(response) {
|
|
|
518
572
|
}
|
|
519
573
|
return new TextDecoder().decode(bytes);
|
|
520
574
|
}
|
|
521
|
-
|
|
575
|
+
function parsePartnerError(payload, status, apiKey) {
|
|
576
|
+
if (!isRecord(payload) || !("error" in payload)) {
|
|
577
|
+
return new PickleballInvalidResponseError();
|
|
578
|
+
}
|
|
579
|
+
if (isRecord(payload.error)) {
|
|
580
|
+
return parseSdkError(payload, status, apiKey);
|
|
581
|
+
}
|
|
582
|
+
if (typeof payload.error !== "string") return new PickleballInvalidResponseError();
|
|
583
|
+
const { error, code, ...details } = payload;
|
|
584
|
+
const safeMessage = apiKey === "" ? error : error.split(apiKey).join("[REDACTED]");
|
|
585
|
+
const partnerCode = typeof code === "string" && code.trim() !== "" ? code : `HTTP_${status}`;
|
|
586
|
+
return new PickleballPartnerApiError(safeMessage, partnerCode, status, details);
|
|
587
|
+
}
|
|
588
|
+
async function decodeResponse(response, apiKey, validate, options = {
|
|
589
|
+
errorStyle: "sdk",
|
|
590
|
+
envelope: "data"
|
|
591
|
+
}) {
|
|
522
592
|
if (response.status >= 300 && response.status < 400) {
|
|
523
593
|
await response.body?.cancel().catch(() => void 0);
|
|
524
594
|
throw new PickleballHttpError(response.status);
|
|
@@ -530,7 +600,10 @@ async function decodeResponse(response, apiKey, validate) {
|
|
|
530
600
|
} catch {
|
|
531
601
|
throw new PickleballInvalidResponseError();
|
|
532
602
|
}
|
|
533
|
-
if (!response.ok)
|
|
603
|
+
if (!response.ok) {
|
|
604
|
+
throw options.errorStyle === "v1" ? parsePartnerError(payload, response.status, apiKey) : parseSdkError(payload, response.status, apiKey);
|
|
605
|
+
}
|
|
606
|
+
if (options.envelope === "raw") return validate(payload);
|
|
534
607
|
if (!isRecord(payload) || !("data" in payload)) {
|
|
535
608
|
throw new PickleballInvalidResponseError();
|
|
536
609
|
}
|
|
@@ -538,7 +611,15 @@ async function decodeResponse(response, apiKey, validate) {
|
|
|
538
611
|
}
|
|
539
612
|
function createPickleballLiveClient(options) {
|
|
540
613
|
const baseUrl = validatedBaseUrl(options.baseUrl);
|
|
541
|
-
const
|
|
614
|
+
const configuredAppId = options.appId === void 0 ? null : requiredNonblank(options.appId, "appId");
|
|
615
|
+
const requireAppId = () => {
|
|
616
|
+
if (configuredAppId === null) {
|
|
617
|
+
throw new PickleballConfigurationError(
|
|
618
|
+
"appId is required for /api/v2/sdk operations (apps/sessions/devices)"
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
return configuredAppId;
|
|
622
|
+
};
|
|
542
623
|
const apiKey = requiredNonblank(options.apiKey, "apiKey");
|
|
543
624
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
544
625
|
if (typeof fetchImplementation !== "function") {
|
|
@@ -551,7 +632,9 @@ function createPickleballLiveClient(options) {
|
|
|
551
632
|
path,
|
|
552
633
|
body,
|
|
553
634
|
retryPolicy,
|
|
554
|
-
validate
|
|
635
|
+
validate,
|
|
636
|
+
errorStyle = "sdk",
|
|
637
|
+
envelope = "data"
|
|
555
638
|
}) {
|
|
556
639
|
const retryLimit = retryPolicy === "transient" ? maxRetries : 0;
|
|
557
640
|
for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
|
|
@@ -572,14 +655,14 @@ function createPickleballLiveClient(options) {
|
|
|
572
655
|
} : {
|
|
573
656
|
accept: "application/json",
|
|
574
657
|
"x-api-key": apiKey,
|
|
575
|
-
"x-pickleball-app-id":
|
|
658
|
+
...configuredAppId === null ? {} : { "x-pickleball-app-id": configuredAppId }
|
|
576
659
|
},
|
|
577
660
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
578
661
|
signal: controller.signal,
|
|
579
662
|
redirect: "error"
|
|
580
663
|
});
|
|
581
664
|
try {
|
|
582
|
-
return await decodeResponse(response, apiKey, validate);
|
|
665
|
+
return await decodeResponse(response, apiKey, validate, { errorStyle, envelope });
|
|
583
666
|
} catch (error) {
|
|
584
667
|
if (isTransientStatus(response.status) && attempt < retryLimit && isRetryableResponseError(error)) {
|
|
585
668
|
clearTimeout(timer);
|
|
@@ -591,7 +674,7 @@ function createPickleballLiveClient(options) {
|
|
|
591
674
|
throw error;
|
|
592
675
|
}
|
|
593
676
|
} catch (error) {
|
|
594
|
-
if (error instanceof PickleballApiError || error instanceof PickleballHttpError || error instanceof PickleballInvalidResponseError) {
|
|
677
|
+
if (error instanceof PickleballApiError || error instanceof PickleballPartnerApiError || error instanceof PickleballHttpError || error instanceof PickleballInvalidResponseError) {
|
|
595
678
|
throw error;
|
|
596
679
|
}
|
|
597
680
|
if (attempt < retryLimit) {
|
|
@@ -610,64 +693,109 @@ function createPickleballLiveClient(options) {
|
|
|
610
693
|
const post = (path, body, retryPolicy, validate) => request({ method: "POST", path, body, retryPolicy, validate });
|
|
611
694
|
const get = (path, validate) => request({ method: "GET", path, retryPolicy: "transient", validate });
|
|
612
695
|
const sessionPath = (sessionId) => `/api/v2/sdk/sessions/${encodeURIComponent(sessionId)}`;
|
|
696
|
+
const v1SessionPath = (sessionId) => `/api/v1/live-sessions/${encodeURIComponent(sessionId)}`;
|
|
697
|
+
const postV1 = (path, body, retryPolicy, validate, envelope = "data") => request({ method: "POST", path, body, retryPolicy, validate, errorStyle: "v1", envelope });
|
|
698
|
+
const getV1 = (path, validate) => request({ method: "GET", path, retryPolicy: "transient", validate, errorStyle: "v1" });
|
|
613
699
|
return {
|
|
614
700
|
apps: {
|
|
615
|
-
bootstrap: (input) => post(
|
|
701
|
+
bootstrap: async (input) => post(
|
|
616
702
|
"/api/v2/sdk/bootstrap",
|
|
617
|
-
{ ...input, appId },
|
|
703
|
+
{ ...input, appId: requireAppId() },
|
|
618
704
|
"transient",
|
|
619
705
|
validateBootstrap
|
|
620
706
|
)
|
|
621
707
|
},
|
|
622
708
|
sessions: {
|
|
623
|
-
start: (input) => post(
|
|
709
|
+
start: async (input) => post(
|
|
624
710
|
"/api/v2/sdk/sessions",
|
|
625
|
-
{ ...input, appId },
|
|
711
|
+
{ ...input, appId: requireAppId() },
|
|
626
712
|
"transient",
|
|
627
713
|
validateGrant
|
|
628
714
|
),
|
|
629
|
-
refresh: (sessionId, input) => post(
|
|
715
|
+
refresh: async (sessionId, input) => post(
|
|
630
716
|
`${sessionPath(sessionId)}/refresh`,
|
|
631
|
-
{ ...input, appId },
|
|
717
|
+
{ ...input, appId: requireAppId() },
|
|
632
718
|
"never",
|
|
633
719
|
validateRefresh
|
|
634
720
|
),
|
|
635
721
|
end: async (input) => {
|
|
636
722
|
await post(
|
|
637
723
|
`${sessionPath(input.sessionId)}/end`,
|
|
638
|
-
{ ...input, appId },
|
|
724
|
+
{ ...input, appId: requireAppId() },
|
|
639
725
|
"transient",
|
|
640
726
|
(value) => validateEnd(value, input.sessionId)
|
|
641
727
|
);
|
|
642
728
|
},
|
|
643
|
-
publish: (sessionId) => post(
|
|
729
|
+
publish: async (sessionId) => post(
|
|
644
730
|
`${sessionPath(sessionId)}/publish`,
|
|
645
|
-
{ sessionId, appId },
|
|
731
|
+
{ sessionId, appId: requireAppId() },
|
|
646
732
|
"transient",
|
|
647
733
|
(value) => validatePublishState(value, sessionId)
|
|
648
734
|
),
|
|
649
|
-
unpublish: (sessionId) => post(
|
|
735
|
+
unpublish: async (sessionId) => post(
|
|
650
736
|
`${sessionPath(sessionId)}/unpublish`,
|
|
651
|
-
{ sessionId, appId },
|
|
737
|
+
{ sessionId, appId: requireAppId() },
|
|
652
738
|
"transient",
|
|
653
739
|
(value) => validatePublishState(value, sessionId)
|
|
654
740
|
),
|
|
655
|
-
get: (sessionId) =>
|
|
656
|
-
|
|
657
|
-
|
|
741
|
+
get: async (sessionId) => {
|
|
742
|
+
requireAppId();
|
|
743
|
+
return get(sessionPath(sessionId), validateSession);
|
|
744
|
+
},
|
|
745
|
+
getRecordings: async (sessionId) => {
|
|
746
|
+
requireAppId();
|
|
747
|
+
return get(
|
|
748
|
+
`${sessionPath(sessionId)}/recordings`,
|
|
749
|
+
(value) => validateRecordings(value, sessionId)
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
liveSessions: {
|
|
754
|
+
createCameraSession: (input) => postV1(
|
|
755
|
+
"/api/v1/live-sessions",
|
|
756
|
+
{
|
|
757
|
+
title: input.title,
|
|
758
|
+
matchRef: input.matchRef,
|
|
759
|
+
cameraGrant: true,
|
|
760
|
+
deliveryMode: "device-rtmp",
|
|
761
|
+
...input.visibility === void 0 ? {} : { visibility: input.visibility },
|
|
762
|
+
...input.description === void 0 ? {} : { description: input.description },
|
|
763
|
+
...input.quality === void 0 ? {} : { quality: input.quality },
|
|
764
|
+
...input.courtRef === void 0 ? {} : { courtRef: input.courtRef }
|
|
765
|
+
},
|
|
766
|
+
// Idempotent theo (host, matchRef): gọi lại chỉ cấp grant mới, an toàn để retry.
|
|
767
|
+
"transient",
|
|
768
|
+
validateCameraSessionCreated
|
|
769
|
+
),
|
|
770
|
+
cameraGrant: (sessionId) => postV1(
|
|
771
|
+
`${v1SessionPath(sessionId)}/camera-grant`,
|
|
772
|
+
{},
|
|
773
|
+
"transient",
|
|
774
|
+
validateCameraGrant
|
|
775
|
+
),
|
|
776
|
+
end: (sessionId) => postV1(
|
|
777
|
+
`${v1SessionPath(sessionId)}/end`,
|
|
778
|
+
{},
|
|
779
|
+
"transient",
|
|
780
|
+
validateLegacyEnd,
|
|
781
|
+
"raw"
|
|
782
|
+
),
|
|
783
|
+
get: (sessionId) => getV1(v1SessionPath(sessionId), validateSession),
|
|
784
|
+
getRecordings: (sessionId) => getV1(
|
|
785
|
+
`${v1SessionPath(sessionId)}/recordings`,
|
|
658
786
|
(value) => validateRecordings(value, sessionId)
|
|
659
787
|
)
|
|
660
788
|
},
|
|
661
789
|
devices: {
|
|
662
|
-
register: (input) => post(
|
|
790
|
+
register: async (input) => post(
|
|
663
791
|
"/api/v2/sdk/devices/register",
|
|
664
|
-
{ ...input, appId },
|
|
792
|
+
{ ...input, appId: requireAppId() },
|
|
665
793
|
"transient",
|
|
666
794
|
validateDeviceRegister
|
|
667
795
|
),
|
|
668
|
-
heartbeat: (input) => post(
|
|
796
|
+
heartbeat: async (input) => post(
|
|
669
797
|
"/api/v2/sdk/devices/heartbeat",
|
|
670
|
-
{ ...input, appId },
|
|
798
|
+
{ ...input, appId: requireAppId() },
|
|
671
799
|
// Heartbeat có side effect trao token 1 lần — không retry để tránh
|
|
672
800
|
// nhận token đã bị đánh dấu delivered ở lần trước
|
|
673
801
|
"never",
|
|
@@ -685,6 +813,7 @@ function createPickleballLiveClient(options) {
|
|
|
685
813
|
PickleballInvalidResponseError,
|
|
686
814
|
PickleballLiveError,
|
|
687
815
|
PickleballNetworkError,
|
|
816
|
+
PickleballPartnerApiError,
|
|
688
817
|
PickleballTimeoutError,
|
|
689
818
|
PickleballWebhookVerificationError,
|
|
690
819
|
SDK_TELEMETRY_EVENT_NAMES,
|