@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/dist/proxy.cjs
CHANGED
|
@@ -50,6 +50,15 @@ var PickleballApiError = class extends PickleballLiveError {
|
|
|
50
50
|
this.name = "PickleballApiError";
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
|
+
var PickleballPartnerApiError = class extends PickleballLiveError {
|
|
54
|
+
constructor(message, partnerCode, status, details = {}) {
|
|
55
|
+
super(message, "PARTNER_API_ERROR");
|
|
56
|
+
this.partnerCode = partnerCode;
|
|
57
|
+
this.status = status;
|
|
58
|
+
this.details = details;
|
|
59
|
+
this.name = "PickleballPartnerApiError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
53
62
|
var PickleballHttpError = class extends PickleballLiveError {
|
|
54
63
|
constructor(status) {
|
|
55
64
|
super(`Request failed with HTTP ${status}`, "HTTP_ERROR");
|
|
@@ -278,6 +287,49 @@ function validateGrant(value) {
|
|
|
278
287
|
config: validateRemoteConfig(data.config)
|
|
279
288
|
};
|
|
280
289
|
}
|
|
290
|
+
function validateCameraGrant(value) {
|
|
291
|
+
const data = record(value);
|
|
292
|
+
const base = validateGrant(data);
|
|
293
|
+
if (!/^rtmps?:\/\//i.test(base.serverUrl)) invalid();
|
|
294
|
+
if (data.status !== "scheduled" && data.status !== "live") invalid();
|
|
295
|
+
return {
|
|
296
|
+
...base,
|
|
297
|
+
status: data.status,
|
|
298
|
+
matchRef: nullableString(data.matchRef),
|
|
299
|
+
courtRef: nullableString(data.courtRef),
|
|
300
|
+
watchUrl: nullableString(data.watchUrl),
|
|
301
|
+
convexUrl: nullableString(data.convexUrl),
|
|
302
|
+
issuedAt: finite(data.issuedAt),
|
|
303
|
+
expiresAt: finite(data.expiresAt)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function validateCameraSessionCreated(value) {
|
|
307
|
+
const data = record(value);
|
|
308
|
+
if (data.status !== "scheduled" && data.status !== "live" && data.status !== "ended") {
|
|
309
|
+
invalid();
|
|
310
|
+
}
|
|
311
|
+
if (data.visibility !== "public" && data.visibility !== "private") invalid();
|
|
312
|
+
if (typeof data.reused !== "boolean") invalid();
|
|
313
|
+
return {
|
|
314
|
+
id: nonblank(data.id),
|
|
315
|
+
status: data.status,
|
|
316
|
+
visibility: data.visibility,
|
|
317
|
+
quality: finite(data.quality),
|
|
318
|
+
deliveryMode: nonblank(data.deliveryMode),
|
|
319
|
+
matchRef: nullableString(data.matchRef),
|
|
320
|
+
courtRef: nullableString(data.courtRef),
|
|
321
|
+
playbackUrl: nullableString(data.playbackUrl),
|
|
322
|
+
whepUrl: nullableString(data.whepUrl),
|
|
323
|
+
watchUrl: nullableString(data.watchUrl),
|
|
324
|
+
reused: data.reused,
|
|
325
|
+
cameraGrant: validateCameraGrant(data.cameraGrant)
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function validateLegacyEnd(value) {
|
|
329
|
+
const data = record(value);
|
|
330
|
+
if (data.ok !== true || data.status !== "ended") invalid();
|
|
331
|
+
return { status: "ended" };
|
|
332
|
+
}
|
|
281
333
|
function validateRefresh(value) {
|
|
282
334
|
const data = record(value);
|
|
283
335
|
return {
|
|
@@ -315,6 +367,7 @@ function validateSession(value) {
|
|
|
315
367
|
matchRef: nullableString(data.matchRef),
|
|
316
368
|
playbackUrl: nullableString(data.playbackUrl),
|
|
317
369
|
watchUrl: nullableString(data.watchUrl),
|
|
370
|
+
whepUrl: nullableString(data.whepUrl),
|
|
318
371
|
scheduledAt: nullableNumber(data.scheduledAt),
|
|
319
372
|
startedAt: nullableNumber(data.startedAt),
|
|
320
373
|
endedAt: nullableNumber(data.endedAt),
|
|
@@ -503,7 +556,23 @@ async function readBoundedResponseText(response2) {
|
|
|
503
556
|
}
|
|
504
557
|
return new TextDecoder().decode(bytes);
|
|
505
558
|
}
|
|
506
|
-
|
|
559
|
+
function parsePartnerError(payload, status, apiKey) {
|
|
560
|
+
if (!isRecord(payload) || !("error" in payload)) {
|
|
561
|
+
return new PickleballInvalidResponseError();
|
|
562
|
+
}
|
|
563
|
+
if (isRecord(payload.error)) {
|
|
564
|
+
return parseSdkError(payload, status, apiKey);
|
|
565
|
+
}
|
|
566
|
+
if (typeof payload.error !== "string") return new PickleballInvalidResponseError();
|
|
567
|
+
const { error, code, ...details } = payload;
|
|
568
|
+
const safeMessage = apiKey === "" ? error : error.split(apiKey).join("[REDACTED]");
|
|
569
|
+
const partnerCode = typeof code === "string" && code.trim() !== "" ? code : `HTTP_${status}`;
|
|
570
|
+
return new PickleballPartnerApiError(safeMessage, partnerCode, status, details);
|
|
571
|
+
}
|
|
572
|
+
async function decodeResponse(response2, apiKey, validate, options = {
|
|
573
|
+
errorStyle: "sdk",
|
|
574
|
+
envelope: "data"
|
|
575
|
+
}) {
|
|
507
576
|
if (response2.status >= 300 && response2.status < 400) {
|
|
508
577
|
await response2.body?.cancel().catch(() => void 0);
|
|
509
578
|
throw new PickleballHttpError(response2.status);
|
|
@@ -515,7 +584,10 @@ async function decodeResponse(response2, apiKey, validate) {
|
|
|
515
584
|
} catch {
|
|
516
585
|
throw new PickleballInvalidResponseError();
|
|
517
586
|
}
|
|
518
|
-
if (!response2.ok)
|
|
587
|
+
if (!response2.ok) {
|
|
588
|
+
throw options.errorStyle === "v1" ? parsePartnerError(payload, response2.status, apiKey) : parseSdkError(payload, response2.status, apiKey);
|
|
589
|
+
}
|
|
590
|
+
if (options.envelope === "raw") return validate(payload);
|
|
519
591
|
if (!isRecord(payload) || !("data" in payload)) {
|
|
520
592
|
throw new PickleballInvalidResponseError();
|
|
521
593
|
}
|
|
@@ -523,7 +595,15 @@ async function decodeResponse(response2, apiKey, validate) {
|
|
|
523
595
|
}
|
|
524
596
|
function createPickleballLiveClient(options) {
|
|
525
597
|
const baseUrl = validatedBaseUrl(options.baseUrl);
|
|
526
|
-
const
|
|
598
|
+
const configuredAppId = options.appId === void 0 ? null : requiredNonblank(options.appId, "appId");
|
|
599
|
+
const requireAppId = () => {
|
|
600
|
+
if (configuredAppId === null) {
|
|
601
|
+
throw new PickleballConfigurationError(
|
|
602
|
+
"appId is required for /api/v2/sdk operations (apps/sessions/devices)"
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
return configuredAppId;
|
|
606
|
+
};
|
|
527
607
|
const apiKey = requiredNonblank(options.apiKey, "apiKey");
|
|
528
608
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
529
609
|
if (typeof fetchImplementation !== "function") {
|
|
@@ -536,7 +616,9 @@ function createPickleballLiveClient(options) {
|
|
|
536
616
|
path,
|
|
537
617
|
body,
|
|
538
618
|
retryPolicy,
|
|
539
|
-
validate
|
|
619
|
+
validate,
|
|
620
|
+
errorStyle = "sdk",
|
|
621
|
+
envelope = "data"
|
|
540
622
|
}) {
|
|
541
623
|
const retryLimit = retryPolicy === "transient" ? maxRetries : 0;
|
|
542
624
|
for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
|
|
@@ -557,14 +639,14 @@ function createPickleballLiveClient(options) {
|
|
|
557
639
|
} : {
|
|
558
640
|
accept: "application/json",
|
|
559
641
|
"x-api-key": apiKey,
|
|
560
|
-
"x-pickleball-app-id":
|
|
642
|
+
...configuredAppId === null ? {} : { "x-pickleball-app-id": configuredAppId }
|
|
561
643
|
},
|
|
562
644
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
563
645
|
signal: controller.signal,
|
|
564
646
|
redirect: "error"
|
|
565
647
|
});
|
|
566
648
|
try {
|
|
567
|
-
return await decodeResponse(response2, apiKey, validate);
|
|
649
|
+
return await decodeResponse(response2, apiKey, validate, { errorStyle, envelope });
|
|
568
650
|
} catch (error) {
|
|
569
651
|
if (isTransientStatus(response2.status) && attempt < retryLimit && isRetryableResponseError(error)) {
|
|
570
652
|
clearTimeout(timer);
|
|
@@ -576,7 +658,7 @@ function createPickleballLiveClient(options) {
|
|
|
576
658
|
throw error;
|
|
577
659
|
}
|
|
578
660
|
} catch (error) {
|
|
579
|
-
if (error instanceof PickleballApiError || error instanceof PickleballHttpError || error instanceof PickleballInvalidResponseError) {
|
|
661
|
+
if (error instanceof PickleballApiError || error instanceof PickleballPartnerApiError || error instanceof PickleballHttpError || error instanceof PickleballInvalidResponseError) {
|
|
580
662
|
throw error;
|
|
581
663
|
}
|
|
582
664
|
if (attempt < retryLimit) {
|
|
@@ -595,64 +677,109 @@ function createPickleballLiveClient(options) {
|
|
|
595
677
|
const post = (path, body, retryPolicy, validate) => request({ method: "POST", path, body, retryPolicy, validate });
|
|
596
678
|
const get = (path, validate) => request({ method: "GET", path, retryPolicy: "transient", validate });
|
|
597
679
|
const sessionPath = (sessionId) => `/api/v2/sdk/sessions/${encodeURIComponent(sessionId)}`;
|
|
680
|
+
const v1SessionPath = (sessionId) => `/api/v1/live-sessions/${encodeURIComponent(sessionId)}`;
|
|
681
|
+
const postV1 = (path, body, retryPolicy, validate, envelope = "data") => request({ method: "POST", path, body, retryPolicy, validate, errorStyle: "v1", envelope });
|
|
682
|
+
const getV1 = (path, validate) => request({ method: "GET", path, retryPolicy: "transient", validate, errorStyle: "v1" });
|
|
598
683
|
return {
|
|
599
684
|
apps: {
|
|
600
|
-
bootstrap: (input) => post(
|
|
685
|
+
bootstrap: async (input) => post(
|
|
601
686
|
"/api/v2/sdk/bootstrap",
|
|
602
|
-
{ ...input, appId },
|
|
687
|
+
{ ...input, appId: requireAppId() },
|
|
603
688
|
"transient",
|
|
604
689
|
validateBootstrap
|
|
605
690
|
)
|
|
606
691
|
},
|
|
607
692
|
sessions: {
|
|
608
|
-
start: (input) => post(
|
|
693
|
+
start: async (input) => post(
|
|
609
694
|
"/api/v2/sdk/sessions",
|
|
610
|
-
{ ...input, appId },
|
|
695
|
+
{ ...input, appId: requireAppId() },
|
|
611
696
|
"transient",
|
|
612
697
|
validateGrant
|
|
613
698
|
),
|
|
614
|
-
refresh: (sessionId, input) => post(
|
|
699
|
+
refresh: async (sessionId, input) => post(
|
|
615
700
|
`${sessionPath(sessionId)}/refresh`,
|
|
616
|
-
{ ...input, appId },
|
|
701
|
+
{ ...input, appId: requireAppId() },
|
|
617
702
|
"never",
|
|
618
703
|
validateRefresh
|
|
619
704
|
),
|
|
620
705
|
end: async (input) => {
|
|
621
706
|
await post(
|
|
622
707
|
`${sessionPath(input.sessionId)}/end`,
|
|
623
|
-
{ ...input, appId },
|
|
708
|
+
{ ...input, appId: requireAppId() },
|
|
624
709
|
"transient",
|
|
625
710
|
(value) => validateEnd(value, input.sessionId)
|
|
626
711
|
);
|
|
627
712
|
},
|
|
628
|
-
publish: (sessionId) => post(
|
|
713
|
+
publish: async (sessionId) => post(
|
|
629
714
|
`${sessionPath(sessionId)}/publish`,
|
|
630
|
-
{ sessionId, appId },
|
|
715
|
+
{ sessionId, appId: requireAppId() },
|
|
631
716
|
"transient",
|
|
632
717
|
(value) => validatePublishState(value, sessionId)
|
|
633
718
|
),
|
|
634
|
-
unpublish: (sessionId) => post(
|
|
719
|
+
unpublish: async (sessionId) => post(
|
|
635
720
|
`${sessionPath(sessionId)}/unpublish`,
|
|
636
|
-
{ sessionId, appId },
|
|
721
|
+
{ sessionId, appId: requireAppId() },
|
|
637
722
|
"transient",
|
|
638
723
|
(value) => validatePublishState(value, sessionId)
|
|
639
724
|
),
|
|
640
|
-
get: (sessionId) =>
|
|
641
|
-
|
|
642
|
-
|
|
725
|
+
get: async (sessionId) => {
|
|
726
|
+
requireAppId();
|
|
727
|
+
return get(sessionPath(sessionId), validateSession);
|
|
728
|
+
},
|
|
729
|
+
getRecordings: async (sessionId) => {
|
|
730
|
+
requireAppId();
|
|
731
|
+
return get(
|
|
732
|
+
`${sessionPath(sessionId)}/recordings`,
|
|
733
|
+
(value) => validateRecordings(value, sessionId)
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
liveSessions: {
|
|
738
|
+
createCameraSession: (input) => postV1(
|
|
739
|
+
"/api/v1/live-sessions",
|
|
740
|
+
{
|
|
741
|
+
title: input.title,
|
|
742
|
+
matchRef: input.matchRef,
|
|
743
|
+
cameraGrant: true,
|
|
744
|
+
deliveryMode: "device-rtmp",
|
|
745
|
+
...input.visibility === void 0 ? {} : { visibility: input.visibility },
|
|
746
|
+
...input.description === void 0 ? {} : { description: input.description },
|
|
747
|
+
...input.quality === void 0 ? {} : { quality: input.quality },
|
|
748
|
+
...input.courtRef === void 0 ? {} : { courtRef: input.courtRef }
|
|
749
|
+
},
|
|
750
|
+
// Idempotent theo (host, matchRef): gọi lại chỉ cấp grant mới, an toàn để retry.
|
|
751
|
+
"transient",
|
|
752
|
+
validateCameraSessionCreated
|
|
753
|
+
),
|
|
754
|
+
cameraGrant: (sessionId) => postV1(
|
|
755
|
+
`${v1SessionPath(sessionId)}/camera-grant`,
|
|
756
|
+
{},
|
|
757
|
+
"transient",
|
|
758
|
+
validateCameraGrant
|
|
759
|
+
),
|
|
760
|
+
end: (sessionId) => postV1(
|
|
761
|
+
`${v1SessionPath(sessionId)}/end`,
|
|
762
|
+
{},
|
|
763
|
+
"transient",
|
|
764
|
+
validateLegacyEnd,
|
|
765
|
+
"raw"
|
|
766
|
+
),
|
|
767
|
+
get: (sessionId) => getV1(v1SessionPath(sessionId), validateSession),
|
|
768
|
+
getRecordings: (sessionId) => getV1(
|
|
769
|
+
`${v1SessionPath(sessionId)}/recordings`,
|
|
643
770
|
(value) => validateRecordings(value, sessionId)
|
|
644
771
|
)
|
|
645
772
|
},
|
|
646
773
|
devices: {
|
|
647
|
-
register: (input) => post(
|
|
774
|
+
register: async (input) => post(
|
|
648
775
|
"/api/v2/sdk/devices/register",
|
|
649
|
-
{ ...input, appId },
|
|
776
|
+
{ ...input, appId: requireAppId() },
|
|
650
777
|
"transient",
|
|
651
778
|
validateDeviceRegister
|
|
652
779
|
),
|
|
653
|
-
heartbeat: (input) => post(
|
|
780
|
+
heartbeat: async (input) => post(
|
|
654
781
|
"/api/v2/sdk/devices/heartbeat",
|
|
655
|
-
{ ...input, appId },
|
|
782
|
+
{ ...input, appId: requireAppId() },
|
|
656
783
|
// Heartbeat có side effect trao token 1 lần — không retry để tránh
|
|
657
784
|
// nhận token đã bị đánh dấu delivered ở lần trước
|
|
658
785
|
"never",
|
|
@@ -793,8 +920,9 @@ function startBody(value) {
|
|
|
793
920
|
"visibility",
|
|
794
921
|
"consentVersion",
|
|
795
922
|
"metadata",
|
|
796
|
-
"standby"
|
|
797
|
-
|
|
923
|
+
"standby",
|
|
924
|
+
"quality"
|
|
925
|
+
]) || !nonblank2(value.externalSessionId, 256) || !nonblank2(value.title, 200) || !nonblank2(value.consentVersion, 100) || value.visibility !== void 0 && value.visibility !== "public" && value.visibility !== "private" || value.standby !== void 0 && typeof value.standby !== "boolean" || value.quality !== void 0 && value.quality !== 720 && value.quality !== 1080) {
|
|
798
926
|
throw new InvalidRequest();
|
|
799
927
|
}
|
|
800
928
|
if (value.metadata !== void 0) {
|
package/dist/proxy.d.cts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { PickleballLiveClient } from './index.cjs';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated Mô hình "proxy do đối tác host" (SDK v2, 6 route + authz gateway)
|
|
5
|
+
* được giữ để tương thích. Tích hợp mới dùng camera grant (ADR-0002):
|
|
6
|
+
* `createPickleballLiveClient().liveSessions.createCameraSession()` ở backend
|
|
7
|
+
* đối tác và `@pickleball/expo-sdk` chế độ grant trên app — không cần proxy.
|
|
8
|
+
*/
|
|
9
|
+
|
|
3
10
|
interface ProxyEnvironment {
|
|
4
11
|
PICKLEBALL_API_BASE_URL: string;
|
|
5
12
|
PICKLEBALL_APP_ID: string;
|
package/dist/proxy.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { PickleballLiveClient } from './index.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated Mô hình "proxy do đối tác host" (SDK v2, 6 route + authz gateway)
|
|
5
|
+
* được giữ để tương thích. Tích hợp mới dùng camera grant (ADR-0002):
|
|
6
|
+
* `createPickleballLiveClient().liveSessions.createCameraSession()` ở backend
|
|
7
|
+
* đối tác và `@pickleball/expo-sdk` chế độ grant trên app — không cần proxy.
|
|
8
|
+
*/
|
|
9
|
+
|
|
3
10
|
interface ProxyEnvironment {
|
|
4
11
|
PICKLEBALL_API_BASE_URL: string;
|
|
5
12
|
PICKLEBALL_APP_ID: string;
|
package/dist/proxy.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
PickleballNetworkError,
|
|
7
7
|
PickleballTimeoutError,
|
|
8
8
|
createPickleballLiveClient
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-UPS4VFER.js";
|
|
10
10
|
|
|
11
11
|
// src/proxy.ts
|
|
12
12
|
var MAX_BODY_BYTES = 16 * 1024;
|
|
@@ -138,8 +138,9 @@ function startBody(value) {
|
|
|
138
138
|
"visibility",
|
|
139
139
|
"consentVersion",
|
|
140
140
|
"metadata",
|
|
141
|
-
"standby"
|
|
142
|
-
|
|
141
|
+
"standby",
|
|
142
|
+
"quality"
|
|
143
|
+
]) || !nonblank(value.externalSessionId, 256) || !nonblank(value.title, 200) || !nonblank(value.consentVersion, 100) || value.visibility !== void 0 && value.visibility !== "public" && value.visibility !== "private" || value.standby !== void 0 && typeof value.standby !== "boolean" || value.quality !== void 0 && value.quality !== 720 && value.quality !== 1080) {
|
|
143
144
|
throw new InvalidRequest();
|
|
144
145
|
}
|
|
145
146
|
if (value.metadata !== void 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pickleball/server-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Framework-free server SDK for Pickleball Live",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "tsup src/index.ts src/proxy.ts --format esm,cjs --dts --clean",
|
|
60
60
|
"test": "vitest run",
|
|
61
|
-
"typecheck": "
|
|
61
|
+
"typecheck": "tsc --noEmit && pnpm run typecheck:nodenext",
|
|
62
62
|
"typecheck:nodenext": "tsc --noEmit -p tsconfig.nodenext.json",
|
|
63
63
|
"verify:pack": "node scripts/verify-pack.mjs"
|
|
64
64
|
}
|