@pickleball/server-sdk 0.1.1 → 0.2.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,21 @@
1
1
  # @pickleball/server-sdk
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Device registry endpoints for the partner proxy: `devices.register` and
8
+ `devices.heartbeat` on `createPickleballLiveClient`, plus the proxy handlers
9
+ `handleDeviceRegister` / `handleDeviceHeartbeat` (routes `/devices/register`
10
+ and `/devices/heartbeat`). The heartbeat response carries a one-time control
11
+ session grant so an operator dashboard can remote-start a livestream on a
12
+ device that is open but idle.
13
+ - New authorization operations `device_register` and `device_heartbeat`.
14
+ **Production authz gateways must allow these two operation names**; the
15
+ staging stub already allows everything.
16
+ - Version aligned with `@pickleball/expo-sdk@0.2.0` (both packages share a
17
+ stable dist-tag promotion).
18
+
3
19
  ## 0.1.1
4
20
 
5
21
  ### Patch Changes
@@ -314,6 +314,29 @@ function validateRecordings(value, sessionId) {
314
314
  if (!Array.isArray(value)) invalid();
315
315
  return value.map((item) => validateRecording(item, sessionId));
316
316
  }
317
+ function validateDeviceRegister(value) {
318
+ const data = record(value);
319
+ const heartbeatIntervalMs = finite(data.heartbeatIntervalMs);
320
+ if (heartbeatIntervalMs <= 0) invalid();
321
+ return {
322
+ deviceId: nonblank(data.deviceId),
323
+ heartbeatIntervalMs
324
+ };
325
+ }
326
+ function validateDeviceHeartbeat(value) {
327
+ const data = record(value);
328
+ if (data.controlSession === null || data.controlSession === void 0) {
329
+ return { controlSession: null };
330
+ }
331
+ const grant = record(data.controlSession);
332
+ return {
333
+ controlSession: {
334
+ id: nonblank(grant.id),
335
+ token: nonblank(grant.token),
336
+ convexUrl: nonblank(grant.convexUrl)
337
+ }
338
+ };
339
+ }
317
340
 
318
341
  // src/contracts.ts
319
342
  var SDK_TELEMETRY_EVENT_NAMES = [
@@ -599,6 +622,22 @@ function createPickleballLiveClient(options) {
599
622
  (value) => validateRecordings(value, sessionId)
600
623
  )
601
624
  },
625
+ devices: {
626
+ register: (input) => post(
627
+ "/api/v2/sdk/devices/register",
628
+ { ...input, appId },
629
+ "transient",
630
+ validateDeviceRegister
631
+ ),
632
+ heartbeat: (input) => post(
633
+ "/api/v2/sdk/devices/heartbeat",
634
+ { ...input, appId },
635
+ // Heartbeat có side effect trao token 1 lần — không retry để tránh
636
+ // nhận token đã bị đánh dấu delivered ở lần trước
637
+ "never",
638
+ validateDeviceHeartbeat
639
+ )
640
+ },
602
641
  webhooks: { verify: verifyWebhook }
603
642
  };
604
643
  }
package/dist/index.cjs CHANGED
@@ -350,6 +350,29 @@ function validateRecordings(value, sessionId) {
350
350
  if (!Array.isArray(value)) invalid();
351
351
  return value.map((item) => validateRecording(item, sessionId));
352
352
  }
353
+ function validateDeviceRegister(value) {
354
+ const data = record(value);
355
+ const heartbeatIntervalMs = finite(data.heartbeatIntervalMs);
356
+ if (heartbeatIntervalMs <= 0) invalid();
357
+ return {
358
+ deviceId: nonblank(data.deviceId),
359
+ heartbeatIntervalMs
360
+ };
361
+ }
362
+ function validateDeviceHeartbeat(value) {
363
+ const data = record(value);
364
+ if (data.controlSession === null || data.controlSession === void 0) {
365
+ return { controlSession: null };
366
+ }
367
+ const grant = record(data.controlSession);
368
+ return {
369
+ controlSession: {
370
+ id: nonblank(grant.id),
371
+ token: nonblank(grant.token),
372
+ convexUrl: nonblank(grant.convexUrl)
373
+ }
374
+ };
375
+ }
353
376
 
354
377
  // src/contracts.ts
355
378
  var SDK_TELEMETRY_EVENT_NAMES = [
@@ -635,6 +658,22 @@ function createPickleballLiveClient(options) {
635
658
  (value) => validateRecordings(value, sessionId)
636
659
  )
637
660
  },
661
+ devices: {
662
+ register: (input) => post(
663
+ "/api/v2/sdk/devices/register",
664
+ { ...input, appId },
665
+ "transient",
666
+ validateDeviceRegister
667
+ ),
668
+ heartbeat: (input) => post(
669
+ "/api/v2/sdk/devices/heartbeat",
670
+ { ...input, appId },
671
+ // Heartbeat có side effect trao token 1 lần — không retry để tránh
672
+ // nhận token đã bị đánh dấu delivered ở lần trước
673
+ "never",
674
+ validateDeviceHeartbeat
675
+ )
676
+ },
638
677
  webhooks: { verify: verifyWebhook }
639
678
  };
640
679
  }
package/dist/index.d.cts CHANGED
@@ -126,6 +126,46 @@ interface ApiRecording {
126
126
  type StartSdkSessionInput = BootstrapInput & StartLivestreamInput & {
127
127
  idempotencyKey: string;
128
128
  };
129
+ /** Thông tin thiết bị kèm khi đăng ký (từ expo-device). */
130
+ interface DeviceInfoInput {
131
+ appVersion?: string;
132
+ deviceName?: string;
133
+ deviceModel?: string;
134
+ osVersion?: string;
135
+ }
136
+ interface DeviceMetricsInput {
137
+ ramFreeMb?: number;
138
+ ramTotalMb?: number;
139
+ storageFreeGb?: number;
140
+ batteryPct?: number;
141
+ batteryTempC?: number;
142
+ thermal?: number;
143
+ netType?: string;
144
+ linkMbps?: number;
145
+ rssi?: number;
146
+ connQuality?: string;
147
+ }
148
+ interface DeviceRegisterResult {
149
+ deviceId: string;
150
+ heartbeatIntervalMs: number;
151
+ }
152
+ interface DeviceHeartbeatInput {
153
+ /** Trạng thái engine hiện tại (idle|standby|live|...). */
154
+ agentState?: string;
155
+ /** Id phiên live đang giữ — null = không có phiên (server clear link). */
156
+ currentSessionId?: string | null;
157
+ appVersion?: string;
158
+ metrics?: DeviceMetricsInput;
159
+ }
160
+ /** Kênh điều khiển được dashboard yêu cầu — app dùng token mở Convex websocket. */
161
+ interface DeviceControlSessionGrant {
162
+ id: string;
163
+ token: string;
164
+ convexUrl: string;
165
+ }
166
+ interface DeviceHeartbeatResult {
167
+ controlSession: DeviceControlSessionGrant | null;
168
+ }
129
169
  declare const SDK_TELEMETRY_EVENT_NAMES: readonly ["state_changed", "connection_quality", "reconnect", "error", "heartbeat", "session_ended"];
130
170
  type SdkTelemetryEventName = (typeof SDK_TELEMETRY_EVENT_NAMES)[number];
131
171
  interface SdkTelemetryEvent {
@@ -197,10 +237,14 @@ interface PickleballLiveClient {
197
237
  get(sessionId: string): Promise<ApiLiveSession>;
198
238
  getRecordings(sessionId: string): Promise<ApiRecording[]>;
199
239
  };
240
+ devices: {
241
+ register(input: BootstrapInput & DeviceInfoInput): Promise<DeviceRegisterResult>;
242
+ heartbeat(input: BootstrapInput & DeviceHeartbeatInput): Promise<DeviceHeartbeatResult>;
243
+ };
200
244
  webhooks: {
201
245
  verify<T extends WebhookPayload = WebhookPayload>(rawBody: WebhookRawBody, headers: WebhookHeaders, secret: string, options?: VerifyWebhookOptions): Promise<VerifiedWebhook<T>>;
202
246
  };
203
247
  }
204
248
  declare function createPickleballLiveClient(options: PickleballLiveClientOptions): PickleballLiveClient;
205
249
 
206
- export { type ApiLiveSession, type ApiRecording, type BootstrapInput, type EndLivestreamInput, type LivestreamSessionProvider, type LivestreamState, PickleballApiError, PickleballConfigurationError, PickleballHttpError, PickleballInvalidResponseError, type PickleballLiveClient, type PickleballLiveClientOptions, PickleballLiveError, PickleballNetworkError, PickleballTimeoutError, PickleballWebhookVerificationError, type RecordingType, SDK_TELEMETRY_EVENT_NAMES, type SdkBootstrap, type SdkError, type SdkErrorCode, type SdkPublishState, type SdkRemoteConfig, type SdkSessionGrant, type SdkSessionRefresh, type SdkTelemetryCredentials, type SdkTelemetryEvent, type SdkTelemetryEventName, type ServerSdkErrorCode, type SessionStatus, type SessionVisibility, type StartLivestreamInput, type StartSdkSessionInput, type VerifiedWebhook, type VerifyWebhookOptions, type WebhookHeaders, type WebhookPayload, type WebhookRawBody, createPickleballLiveClient, verifyWebhook };
250
+ export { type ApiLiveSession, type ApiRecording, type BootstrapInput, type DeviceControlSessionGrant, type DeviceHeartbeatInput, type DeviceHeartbeatResult, type DeviceInfoInput, type DeviceMetricsInput, type DeviceRegisterResult, type EndLivestreamInput, type LivestreamSessionProvider, type LivestreamState, PickleballApiError, PickleballConfigurationError, PickleballHttpError, PickleballInvalidResponseError, type PickleballLiveClient, type PickleballLiveClientOptions, PickleballLiveError, PickleballNetworkError, PickleballTimeoutError, PickleballWebhookVerificationError, type RecordingType, SDK_TELEMETRY_EVENT_NAMES, type SdkBootstrap, type SdkError, type SdkErrorCode, type SdkPublishState, type SdkRemoteConfig, type SdkSessionGrant, type SdkSessionRefresh, type SdkTelemetryCredentials, type SdkTelemetryEvent, type SdkTelemetryEventName, type ServerSdkErrorCode, type SessionStatus, type SessionVisibility, type StartLivestreamInput, type StartSdkSessionInput, type VerifiedWebhook, type VerifyWebhookOptions, type WebhookHeaders, type WebhookPayload, type WebhookRawBody, createPickleballLiveClient, verifyWebhook };
package/dist/index.d.ts CHANGED
@@ -126,6 +126,46 @@ interface ApiRecording {
126
126
  type StartSdkSessionInput = BootstrapInput & StartLivestreamInput & {
127
127
  idempotencyKey: string;
128
128
  };
129
+ /** Thông tin thiết bị kèm khi đăng ký (từ expo-device). */
130
+ interface DeviceInfoInput {
131
+ appVersion?: string;
132
+ deviceName?: string;
133
+ deviceModel?: string;
134
+ osVersion?: string;
135
+ }
136
+ interface DeviceMetricsInput {
137
+ ramFreeMb?: number;
138
+ ramTotalMb?: number;
139
+ storageFreeGb?: number;
140
+ batteryPct?: number;
141
+ batteryTempC?: number;
142
+ thermal?: number;
143
+ netType?: string;
144
+ linkMbps?: number;
145
+ rssi?: number;
146
+ connQuality?: string;
147
+ }
148
+ interface DeviceRegisterResult {
149
+ deviceId: string;
150
+ heartbeatIntervalMs: number;
151
+ }
152
+ interface DeviceHeartbeatInput {
153
+ /** Trạng thái engine hiện tại (idle|standby|live|...). */
154
+ agentState?: string;
155
+ /** Id phiên live đang giữ — null = không có phiên (server clear link). */
156
+ currentSessionId?: string | null;
157
+ appVersion?: string;
158
+ metrics?: DeviceMetricsInput;
159
+ }
160
+ /** Kênh điều khiển được dashboard yêu cầu — app dùng token mở Convex websocket. */
161
+ interface DeviceControlSessionGrant {
162
+ id: string;
163
+ token: string;
164
+ convexUrl: string;
165
+ }
166
+ interface DeviceHeartbeatResult {
167
+ controlSession: DeviceControlSessionGrant | null;
168
+ }
129
169
  declare const SDK_TELEMETRY_EVENT_NAMES: readonly ["state_changed", "connection_quality", "reconnect", "error", "heartbeat", "session_ended"];
130
170
  type SdkTelemetryEventName = (typeof SDK_TELEMETRY_EVENT_NAMES)[number];
131
171
  interface SdkTelemetryEvent {
@@ -197,10 +237,14 @@ interface PickleballLiveClient {
197
237
  get(sessionId: string): Promise<ApiLiveSession>;
198
238
  getRecordings(sessionId: string): Promise<ApiRecording[]>;
199
239
  };
240
+ devices: {
241
+ register(input: BootstrapInput & DeviceInfoInput): Promise<DeviceRegisterResult>;
242
+ heartbeat(input: BootstrapInput & DeviceHeartbeatInput): Promise<DeviceHeartbeatResult>;
243
+ };
200
244
  webhooks: {
201
245
  verify<T extends WebhookPayload = WebhookPayload>(rawBody: WebhookRawBody, headers: WebhookHeaders, secret: string, options?: VerifyWebhookOptions): Promise<VerifiedWebhook<T>>;
202
246
  };
203
247
  }
204
248
  declare function createPickleballLiveClient(options: PickleballLiveClientOptions): PickleballLiveClient;
205
249
 
206
- export { type ApiLiveSession, type ApiRecording, type BootstrapInput, type EndLivestreamInput, type LivestreamSessionProvider, type LivestreamState, PickleballApiError, PickleballConfigurationError, PickleballHttpError, PickleballInvalidResponseError, type PickleballLiveClient, type PickleballLiveClientOptions, PickleballLiveError, PickleballNetworkError, PickleballTimeoutError, PickleballWebhookVerificationError, type RecordingType, SDK_TELEMETRY_EVENT_NAMES, type SdkBootstrap, type SdkError, type SdkErrorCode, type SdkPublishState, type SdkRemoteConfig, type SdkSessionGrant, type SdkSessionRefresh, type SdkTelemetryCredentials, type SdkTelemetryEvent, type SdkTelemetryEventName, type ServerSdkErrorCode, type SessionStatus, type SessionVisibility, type StartLivestreamInput, type StartSdkSessionInput, type VerifiedWebhook, type VerifyWebhookOptions, type WebhookHeaders, type WebhookPayload, type WebhookRawBody, createPickleballLiveClient, verifyWebhook };
250
+ export { type ApiLiveSession, type ApiRecording, type BootstrapInput, type DeviceControlSessionGrant, type DeviceHeartbeatInput, type DeviceHeartbeatResult, type DeviceInfoInput, type DeviceMetricsInput, type DeviceRegisterResult, type EndLivestreamInput, type LivestreamSessionProvider, type LivestreamState, PickleballApiError, PickleballConfigurationError, PickleballHttpError, PickleballInvalidResponseError, type PickleballLiveClient, type PickleballLiveClientOptions, PickleballLiveError, PickleballNetworkError, PickleballTimeoutError, PickleballWebhookVerificationError, type RecordingType, SDK_TELEMETRY_EVENT_NAMES, type SdkBootstrap, type SdkError, type SdkErrorCode, type SdkPublishState, type SdkRemoteConfig, type SdkSessionGrant, type SdkSessionRefresh, type SdkTelemetryCredentials, type SdkTelemetryEvent, type SdkTelemetryEventName, type ServerSdkErrorCode, type SessionStatus, type SessionVisibility, type StartLivestreamInput, type StartSdkSessionInput, type VerifiedWebhook, type VerifyWebhookOptions, type WebhookHeaders, type WebhookPayload, type WebhookRawBody, createPickleballLiveClient, verifyWebhook };
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  SDK_TELEMETRY_EVENT_NAMES,
11
11
  createPickleballLiveClient,
12
12
  verifyWebhook
13
- } from "./chunk-HBTNLURN.js";
13
+ } from "./chunk-Y635I73O.js";
14
14
  export {
15
15
  PickleballApiError,
16
16
  PickleballConfigurationError,
package/dist/proxy.cjs CHANGED
@@ -22,6 +22,8 @@ var proxy_exports = {};
22
22
  __export(proxy_exports, {
23
23
  createStableIdempotencyKey: () => createStableIdempotencyKey,
24
24
  handleBootstrap: () => handleBootstrap,
25
+ handleDeviceHeartbeat: () => handleDeviceHeartbeat,
26
+ handleDeviceRegister: () => handleDeviceRegister,
25
27
  handleSessionAction: () => handleSessionAction,
26
28
  handleSessions: () => handleSessions
27
29
  });
@@ -343,6 +345,29 @@ function validateRecordings(value, sessionId) {
343
345
  if (!Array.isArray(value)) invalid();
344
346
  return value.map((item) => validateRecording(item, sessionId));
345
347
  }
348
+ function validateDeviceRegister(value) {
349
+ const data = record(value);
350
+ const heartbeatIntervalMs = finite(data.heartbeatIntervalMs);
351
+ if (heartbeatIntervalMs <= 0) invalid();
352
+ return {
353
+ deviceId: nonblank(data.deviceId),
354
+ heartbeatIntervalMs
355
+ };
356
+ }
357
+ function validateDeviceHeartbeat(value) {
358
+ const data = record(value);
359
+ if (data.controlSession === null || data.controlSession === void 0) {
360
+ return { controlSession: null };
361
+ }
362
+ const grant = record(data.controlSession);
363
+ return {
364
+ controlSession: {
365
+ id: nonblank(grant.id),
366
+ token: nonblank(grant.token),
367
+ convexUrl: nonblank(grant.convexUrl)
368
+ }
369
+ };
370
+ }
346
371
 
347
372
  // src/index.ts
348
373
  var TRANSIENT_STATUSES = /* @__PURE__ */ new Set([408, 429]);
@@ -618,6 +643,22 @@ function createPickleballLiveClient(options) {
618
643
  (value) => validateRecordings(value, sessionId)
619
644
  )
620
645
  },
646
+ devices: {
647
+ register: (input) => post(
648
+ "/api/v2/sdk/devices/register",
649
+ { ...input, appId },
650
+ "transient",
651
+ validateDeviceRegister
652
+ ),
653
+ heartbeat: (input) => post(
654
+ "/api/v2/sdk/devices/heartbeat",
655
+ { ...input, appId },
656
+ // Heartbeat có side effect trao token 1 lần — không retry để tránh
657
+ // nhận token đã bị đánh dấu delivered ở lần trước
658
+ "never",
659
+ validateDeviceHeartbeat
660
+ )
661
+ },
621
662
  webhooks: { verify: verifyWebhook }
622
663
  };
623
664
  }
@@ -1009,10 +1050,139 @@ async function handleSessionAction(request, params, deps) {
1009
1050
  return null;
1010
1051
  });
1011
1052
  }
1053
+ var DEVICE_INFO_FIELDS = ["appVersion", "deviceName", "deviceModel", "osVersion"];
1054
+ var DEVICE_METRIC_NUMBER_FIELDS = [
1055
+ "ramFreeMb",
1056
+ "ramTotalMb",
1057
+ "storageFreeGb",
1058
+ "batteryPct",
1059
+ "batteryTempC",
1060
+ "thermal",
1061
+ "linkMbps",
1062
+ "rssi"
1063
+ ];
1064
+ var DEVICE_METRIC_STRING_FIELDS = ["netType", "connQuality"];
1065
+ function deviceInfoFields(value) {
1066
+ const info = {};
1067
+ for (const field of DEVICE_INFO_FIELDS) {
1068
+ const entry = value[field];
1069
+ if (entry === void 0) continue;
1070
+ if (!nonblank2(entry, 200)) throw new InvalidRequest();
1071
+ info[field] = entry;
1072
+ }
1073
+ return info;
1074
+ }
1075
+ function deviceRegisterBody(value) {
1076
+ if (!hasOnlyKeys(value, [
1077
+ "sdkVersion",
1078
+ "platform",
1079
+ "bundleId",
1080
+ "installationId",
1081
+ ...DEVICE_INFO_FIELDS
1082
+ ])) {
1083
+ throw new InvalidRequest();
1084
+ }
1085
+ const identity = bootstrapBody({
1086
+ sdkVersion: value.sdkVersion,
1087
+ platform: value.platform,
1088
+ bundleId: value.bundleId,
1089
+ installationId: value.installationId
1090
+ });
1091
+ return { ...identity, ...deviceInfoFields(value) };
1092
+ }
1093
+ function deviceMetricsFields(value) {
1094
+ if (value === void 0) return void 0;
1095
+ if (!isObject(value)) throw new InvalidRequest();
1096
+ if (!hasOnlyKeys(value, [...DEVICE_METRIC_NUMBER_FIELDS, ...DEVICE_METRIC_STRING_FIELDS])) {
1097
+ throw new InvalidRequest();
1098
+ }
1099
+ const metrics = {};
1100
+ for (const field of DEVICE_METRIC_NUMBER_FIELDS) {
1101
+ const entry = value[field];
1102
+ if (entry === void 0) continue;
1103
+ if (typeof entry !== "number" || !Number.isFinite(entry)) throw new InvalidRequest();
1104
+ metrics[field] = entry;
1105
+ }
1106
+ for (const field of DEVICE_METRIC_STRING_FIELDS) {
1107
+ const entry = value[field];
1108
+ if (entry === void 0) continue;
1109
+ if (!nonblank2(entry, 50)) throw new InvalidRequest();
1110
+ metrics[field] = entry;
1111
+ }
1112
+ return metrics;
1113
+ }
1114
+ function deviceHeartbeatBody(value) {
1115
+ if (!hasOnlyKeys(value, [
1116
+ "sdkVersion",
1117
+ "platform",
1118
+ "bundleId",
1119
+ "installationId",
1120
+ "agentState",
1121
+ "currentSessionId",
1122
+ "appVersion",
1123
+ "metrics"
1124
+ ])) {
1125
+ throw new InvalidRequest();
1126
+ }
1127
+ const identity = bootstrapBody({
1128
+ sdkVersion: value.sdkVersion,
1129
+ platform: value.platform,
1130
+ bundleId: value.bundleId,
1131
+ installationId: value.installationId
1132
+ });
1133
+ if (value.agentState !== void 0 && !nonblank2(value.agentState, 50)) {
1134
+ throw new InvalidRequest();
1135
+ }
1136
+ if (value.currentSessionId !== void 0 && value.currentSessionId !== null && !nonblank2(value.currentSessionId, 256)) {
1137
+ throw new InvalidRequest();
1138
+ }
1139
+ if (value.appVersion !== void 0 && !nonblank2(value.appVersion, 200)) {
1140
+ throw new InvalidRequest();
1141
+ }
1142
+ const metrics = deviceMetricsFields(value.metrics);
1143
+ return {
1144
+ ...identity,
1145
+ ...value.agentState === void 0 ? {} : { agentState: value.agentState },
1146
+ ...value.currentSessionId === void 0 ? {} : { currentSessionId: value.currentSessionId },
1147
+ ...value.appVersion === void 0 ? {} : { appVersion: value.appVersion },
1148
+ ...metrics === void 0 ? {} : { metrics }
1149
+ };
1150
+ }
1151
+ function deviceClientFor(deps) {
1152
+ const client = clientFor(deps);
1153
+ if (!client.devices) {
1154
+ throw new PickleballConfigurationError("Proxy client does not support devices");
1155
+ }
1156
+ return client.devices;
1157
+ }
1158
+ async function handleDeviceRegister(request, deps) {
1159
+ return execute(deps, async () => {
1160
+ const authorization = userAuthorization(request.headers);
1161
+ const attestation = appAttestation(request.headers);
1162
+ const identity = identityFromHeaders(request.headers);
1163
+ const input = deviceRegisterBody(await bodyObject(request));
1164
+ if (!identitiesMatch(identity, input)) throw new InvalidRequest();
1165
+ await authorize(deps, authorization, "device_register", identity, {}, attestation);
1166
+ return deviceClientFor(deps).register(input);
1167
+ });
1168
+ }
1169
+ async function handleDeviceHeartbeat(request, deps) {
1170
+ return execute(deps, async () => {
1171
+ const authorization = userAuthorization(request.headers);
1172
+ const attestation = appAttestation(request.headers);
1173
+ const identity = identityFromHeaders(request.headers);
1174
+ const input = deviceHeartbeatBody(await bodyObject(request));
1175
+ if (!identitiesMatch(identity, input)) throw new InvalidRequest();
1176
+ await authorize(deps, authorization, "device_heartbeat", identity, {}, attestation);
1177
+ return deviceClientFor(deps).heartbeat(input);
1178
+ });
1179
+ }
1012
1180
  // Annotate the CommonJS export names for ESM import in node:
1013
1181
  0 && (module.exports = {
1014
1182
  createStableIdempotencyKey,
1015
1183
  handleBootstrap,
1184
+ handleDeviceHeartbeat,
1185
+ handleDeviceRegister,
1016
1186
  handleSessionAction,
1017
1187
  handleSessions
1018
1188
  });
package/dist/proxy.d.cts CHANGED
@@ -21,6 +21,7 @@ interface ProxyDependencies {
21
21
  type ProxyClient = {
22
22
  apps: Pick<PickleballLiveClient["apps"], "bootstrap">;
23
23
  sessions: Pick<PickleballLiveClient["sessions"], "start" | "refresh" | "end" | "publish" | "unpublish">;
24
+ devices?: Pick<PickleballLiveClient["devices"], "register" | "heartbeat">;
24
25
  };
25
26
  interface SessionActionParams {
26
27
  id: string;
@@ -35,5 +36,7 @@ declare function createStableIdempotencyKey(input: {
35
36
  declare function handleBootstrap(request: Request, deps: ProxyDependencies): Promise<Response>;
36
37
  declare function handleSessions(request: Request, deps: ProxyDependencies): Promise<Response>;
37
38
  declare function handleSessionAction(request: Request, params: SessionActionParams, deps: ProxyDependencies): Promise<Response>;
39
+ declare function handleDeviceRegister(request: Request, deps: ProxyDependencies): Promise<Response>;
40
+ declare function handleDeviceHeartbeat(request: Request, deps: ProxyDependencies): Promise<Response>;
38
41
 
39
- export { type ProxyDependencies, type ProxyEnvironment, type SessionActionParams, createStableIdempotencyKey, handleBootstrap, handleSessionAction, handleSessions };
42
+ export { type ProxyDependencies, type ProxyEnvironment, type SessionActionParams, createStableIdempotencyKey, handleBootstrap, handleDeviceHeartbeat, handleDeviceRegister, handleSessionAction, handleSessions };
package/dist/proxy.d.ts CHANGED
@@ -21,6 +21,7 @@ interface ProxyDependencies {
21
21
  type ProxyClient = {
22
22
  apps: Pick<PickleballLiveClient["apps"], "bootstrap">;
23
23
  sessions: Pick<PickleballLiveClient["sessions"], "start" | "refresh" | "end" | "publish" | "unpublish">;
24
+ devices?: Pick<PickleballLiveClient["devices"], "register" | "heartbeat">;
24
25
  };
25
26
  interface SessionActionParams {
26
27
  id: string;
@@ -35,5 +36,7 @@ declare function createStableIdempotencyKey(input: {
35
36
  declare function handleBootstrap(request: Request, deps: ProxyDependencies): Promise<Response>;
36
37
  declare function handleSessions(request: Request, deps: ProxyDependencies): Promise<Response>;
37
38
  declare function handleSessionAction(request: Request, params: SessionActionParams, deps: ProxyDependencies): Promise<Response>;
39
+ declare function handleDeviceRegister(request: Request, deps: ProxyDependencies): Promise<Response>;
40
+ declare function handleDeviceHeartbeat(request: Request, deps: ProxyDependencies): Promise<Response>;
38
41
 
39
- export { type ProxyDependencies, type ProxyEnvironment, type SessionActionParams, createStableIdempotencyKey, handleBootstrap, handleSessionAction, handleSessions };
42
+ export { type ProxyDependencies, type ProxyEnvironment, type SessionActionParams, createStableIdempotencyKey, handleBootstrap, handleDeviceHeartbeat, handleDeviceRegister, handleSessionAction, handleSessions };
package/dist/proxy.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  PickleballNetworkError,
7
7
  PickleballTimeoutError,
8
8
  createPickleballLiveClient
9
- } from "./chunk-HBTNLURN.js";
9
+ } from "./chunk-Y635I73O.js";
10
10
 
11
11
  // src/proxy.ts
12
12
  var MAX_BODY_BYTES = 16 * 1024;
@@ -395,9 +395,138 @@ async function handleSessionAction(request, params, deps) {
395
395
  return null;
396
396
  });
397
397
  }
398
+ var DEVICE_INFO_FIELDS = ["appVersion", "deviceName", "deviceModel", "osVersion"];
399
+ var DEVICE_METRIC_NUMBER_FIELDS = [
400
+ "ramFreeMb",
401
+ "ramTotalMb",
402
+ "storageFreeGb",
403
+ "batteryPct",
404
+ "batteryTempC",
405
+ "thermal",
406
+ "linkMbps",
407
+ "rssi"
408
+ ];
409
+ var DEVICE_METRIC_STRING_FIELDS = ["netType", "connQuality"];
410
+ function deviceInfoFields(value) {
411
+ const info = {};
412
+ for (const field of DEVICE_INFO_FIELDS) {
413
+ const entry = value[field];
414
+ if (entry === void 0) continue;
415
+ if (!nonblank(entry, 200)) throw new InvalidRequest();
416
+ info[field] = entry;
417
+ }
418
+ return info;
419
+ }
420
+ function deviceRegisterBody(value) {
421
+ if (!hasOnlyKeys(value, [
422
+ "sdkVersion",
423
+ "platform",
424
+ "bundleId",
425
+ "installationId",
426
+ ...DEVICE_INFO_FIELDS
427
+ ])) {
428
+ throw new InvalidRequest();
429
+ }
430
+ const identity = bootstrapBody({
431
+ sdkVersion: value.sdkVersion,
432
+ platform: value.platform,
433
+ bundleId: value.bundleId,
434
+ installationId: value.installationId
435
+ });
436
+ return { ...identity, ...deviceInfoFields(value) };
437
+ }
438
+ function deviceMetricsFields(value) {
439
+ if (value === void 0) return void 0;
440
+ if (!isObject(value)) throw new InvalidRequest();
441
+ if (!hasOnlyKeys(value, [...DEVICE_METRIC_NUMBER_FIELDS, ...DEVICE_METRIC_STRING_FIELDS])) {
442
+ throw new InvalidRequest();
443
+ }
444
+ const metrics = {};
445
+ for (const field of DEVICE_METRIC_NUMBER_FIELDS) {
446
+ const entry = value[field];
447
+ if (entry === void 0) continue;
448
+ if (typeof entry !== "number" || !Number.isFinite(entry)) throw new InvalidRequest();
449
+ metrics[field] = entry;
450
+ }
451
+ for (const field of DEVICE_METRIC_STRING_FIELDS) {
452
+ const entry = value[field];
453
+ if (entry === void 0) continue;
454
+ if (!nonblank(entry, 50)) throw new InvalidRequest();
455
+ metrics[field] = entry;
456
+ }
457
+ return metrics;
458
+ }
459
+ function deviceHeartbeatBody(value) {
460
+ if (!hasOnlyKeys(value, [
461
+ "sdkVersion",
462
+ "platform",
463
+ "bundleId",
464
+ "installationId",
465
+ "agentState",
466
+ "currentSessionId",
467
+ "appVersion",
468
+ "metrics"
469
+ ])) {
470
+ throw new InvalidRequest();
471
+ }
472
+ const identity = bootstrapBody({
473
+ sdkVersion: value.sdkVersion,
474
+ platform: value.platform,
475
+ bundleId: value.bundleId,
476
+ installationId: value.installationId
477
+ });
478
+ if (value.agentState !== void 0 && !nonblank(value.agentState, 50)) {
479
+ throw new InvalidRequest();
480
+ }
481
+ if (value.currentSessionId !== void 0 && value.currentSessionId !== null && !nonblank(value.currentSessionId, 256)) {
482
+ throw new InvalidRequest();
483
+ }
484
+ if (value.appVersion !== void 0 && !nonblank(value.appVersion, 200)) {
485
+ throw new InvalidRequest();
486
+ }
487
+ const metrics = deviceMetricsFields(value.metrics);
488
+ return {
489
+ ...identity,
490
+ ...value.agentState === void 0 ? {} : { agentState: value.agentState },
491
+ ...value.currentSessionId === void 0 ? {} : { currentSessionId: value.currentSessionId },
492
+ ...value.appVersion === void 0 ? {} : { appVersion: value.appVersion },
493
+ ...metrics === void 0 ? {} : { metrics }
494
+ };
495
+ }
496
+ function deviceClientFor(deps) {
497
+ const client = clientFor(deps);
498
+ if (!client.devices) {
499
+ throw new PickleballConfigurationError("Proxy client does not support devices");
500
+ }
501
+ return client.devices;
502
+ }
503
+ async function handleDeviceRegister(request, deps) {
504
+ return execute(deps, async () => {
505
+ const authorization = userAuthorization(request.headers);
506
+ const attestation = appAttestation(request.headers);
507
+ const identity = identityFromHeaders(request.headers);
508
+ const input = deviceRegisterBody(await bodyObject(request));
509
+ if (!identitiesMatch(identity, input)) throw new InvalidRequest();
510
+ await authorize(deps, authorization, "device_register", identity, {}, attestation);
511
+ return deviceClientFor(deps).register(input);
512
+ });
513
+ }
514
+ async function handleDeviceHeartbeat(request, deps) {
515
+ return execute(deps, async () => {
516
+ const authorization = userAuthorization(request.headers);
517
+ const attestation = appAttestation(request.headers);
518
+ const identity = identityFromHeaders(request.headers);
519
+ const input = deviceHeartbeatBody(await bodyObject(request));
520
+ if (!identitiesMatch(identity, input)) throw new InvalidRequest();
521
+ await authorize(deps, authorization, "device_heartbeat", identity, {}, attestation);
522
+ return deviceClientFor(deps).heartbeat(input);
523
+ });
524
+ }
398
525
  export {
399
526
  createStableIdempotencyKey,
400
527
  handleBootstrap,
528
+ handleDeviceHeartbeat,
529
+ handleDeviceRegister,
401
530
  handleSessionAction,
402
531
  handleSessions
403
532
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pickleball/server-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Framework-free server SDK for Pickleball Live",
5
5
  "license": "MIT",
6
6
  "repository": {