@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/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
- async function decodeResponse(response2, apiKey, validate) {
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) throw parseSdkError(payload, response2.status, apiKey);
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 appId = requiredNonblank(options.appId, "appId");
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": appId
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) => get(sessionPath(sessionId), validateSession),
641
- getRecordings: (sessionId) => get(
642
- `${sessionPath(sessionId)}/recordings`,
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
- ]) || !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") {
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-Y635I73O.js";
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
- ]) || !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") {
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.2.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": "pnpm run build && tsc --noEmit && pnpm run typecheck:nodenext",
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
  }