@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 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
- async function decodeResponse(response, apiKey, validate) {
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) throw parseSdkError(payload, response.status, apiKey);
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 appId = requiredNonblank(options.appId, "appId");
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": appId
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) => get(sessionPath(sessionId), validateSession),
620
- getRecordings: (sessionId) => get(
621
- `${sessionPath(sessionId)}/recordings`,
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
- async function decodeResponse(response, apiKey, validate) {
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) throw parseSdkError(payload, response.status, apiKey);
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 appId = requiredNonblank(options.appId, "appId");
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": appId
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) => get(sessionPath(sessionId), validateSession),
656
- getRecordings: (sessionId) => get(
657
- `${sessionPath(sessionId)}/recordings`,
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,