@pickleball/server-sdk 0.1.1
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 +13 -0
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/chunk-HBTNLURN.js +618 -0
- package/dist/index.cjs +654 -0
- package/dist/index.d.cts +206 -0
- package/dist/index.d.ts +206 -0
- package/dist/index.js +26 -0
- package/dist/proxy.cjs +1018 -0
- package/dist/proxy.d.cts +39 -0
- package/dist/proxy.d.ts +39 -0
- package/dist/proxy.js +403 -0
- package/package.json +65 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var PickleballLiveError = class extends Error {
|
|
3
|
+
constructor(message, code, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = "PickleballLiveError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var PickleballConfigurationError = class extends PickleballLiveError {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message, "CONFIGURATION_ERROR");
|
|
12
|
+
this.name = "PickleballConfigurationError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var PickleballApiError = class extends PickleballLiveError {
|
|
16
|
+
constructor(message, code, status) {
|
|
17
|
+
super(message, code);
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.name = "PickleballApiError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var PickleballHttpError = class extends PickleballLiveError {
|
|
23
|
+
constructor(status) {
|
|
24
|
+
super(`Request failed with HTTP ${status}`, "HTTP_ERROR");
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.name = "PickleballHttpError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var PickleballInvalidResponseError = class extends PickleballLiveError {
|
|
30
|
+
constructor() {
|
|
31
|
+
super("The server returned an invalid response", "INVALID_RESPONSE");
|
|
32
|
+
this.name = "PickleballInvalidResponseError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var PickleballTimeoutError = class extends PickleballLiveError {
|
|
36
|
+
constructor() {
|
|
37
|
+
super("The request timed out", "TIMEOUT");
|
|
38
|
+
this.name = "PickleballTimeoutError";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var PickleballNetworkError = class extends PickleballLiveError {
|
|
42
|
+
constructor() {
|
|
43
|
+
super("The request failed because of a network error", "NETWORK_ERROR");
|
|
44
|
+
this.name = "PickleballNetworkError";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var PickleballWebhookVerificationError = class extends PickleballLiveError {
|
|
48
|
+
constructor(message = "Webhook verification failed") {
|
|
49
|
+
super(message, "WEBHOOK_VERIFICATION_FAILED");
|
|
50
|
+
this.name = "PickleballWebhookVerificationError";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/webhooks.ts
|
|
55
|
+
function fail() {
|
|
56
|
+
throw new PickleballWebhookVerificationError();
|
|
57
|
+
}
|
|
58
|
+
function headerValue(headers, name) {
|
|
59
|
+
if (headers instanceof Headers) return headers.get(name);
|
|
60
|
+
const entry = Object.entries(headers).find(([key]) => key.toLowerCase() === name);
|
|
61
|
+
if (!entry) return null;
|
|
62
|
+
const value = entry[1];
|
|
63
|
+
return Array.isArray(value) ? value.join(",") : value ?? null;
|
|
64
|
+
}
|
|
65
|
+
function bodyBytes(rawBody) {
|
|
66
|
+
return typeof rawBody === "string" ? new TextEncoder().encode(rawBody) : rawBody;
|
|
67
|
+
}
|
|
68
|
+
function signedBytes(timestamp, rawBody) {
|
|
69
|
+
const prefix = new TextEncoder().encode(`${timestamp}.`);
|
|
70
|
+
const body = bodyBytes(rawBody);
|
|
71
|
+
const result = new Uint8Array(prefix.length + body.length);
|
|
72
|
+
result.set(prefix);
|
|
73
|
+
result.set(body, prefix.length);
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function decodeHex(value) {
|
|
77
|
+
if (!/^[0-9a-fA-F]{64}$/.test(value)) return null;
|
|
78
|
+
const bytes = new Uint8Array(value.length / 2);
|
|
79
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
80
|
+
bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
|
|
81
|
+
}
|
|
82
|
+
return bytes;
|
|
83
|
+
}
|
|
84
|
+
function constantTimeEqual(left, right) {
|
|
85
|
+
let difference = left.length ^ right.length;
|
|
86
|
+
const length = Math.max(left.length, right.length);
|
|
87
|
+
for (let index = 0; index < length; index += 1) {
|
|
88
|
+
difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
|
|
89
|
+
}
|
|
90
|
+
return difference === 0;
|
|
91
|
+
}
|
|
92
|
+
function encodeHex(value) {
|
|
93
|
+
return Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
94
|
+
}
|
|
95
|
+
function parseSignature(header) {
|
|
96
|
+
let timestamp = null;
|
|
97
|
+
const signatures = [];
|
|
98
|
+
for (const field of header.split(",")) {
|
|
99
|
+
const separator = field.indexOf("=");
|
|
100
|
+
if (separator === -1) continue;
|
|
101
|
+
const key = field.slice(0, separator).trim();
|
|
102
|
+
const value = field.slice(separator + 1).trim();
|
|
103
|
+
if (key === "t" && /^\d+$/.test(value)) timestamp = Number(value);
|
|
104
|
+
if (key === "v1") {
|
|
105
|
+
const decoded = decodeHex(value);
|
|
106
|
+
if (decoded) signatures.push(decoded);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (timestamp === null || !Number.isSafeInteger(timestamp) || signatures.length === 0) fail();
|
|
110
|
+
return { timestamp, signatures };
|
|
111
|
+
}
|
|
112
|
+
function decodeJson(rawBody) {
|
|
113
|
+
let text;
|
|
114
|
+
try {
|
|
115
|
+
text = typeof rawBody === "string" ? rawBody : new TextDecoder("utf-8", { fatal: true }).decode(rawBody);
|
|
116
|
+
return JSON.parse(text);
|
|
117
|
+
} catch {
|
|
118
|
+
fail();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function verifyWebhook(rawBody, headers, secret, options = {}) {
|
|
122
|
+
const id = headerValue(headers, "webhook-id");
|
|
123
|
+
const transportEvent = headerValue(headers, "webhook-event");
|
|
124
|
+
const signatureHeader = headerValue(headers, "webhook-signature");
|
|
125
|
+
if (!id?.trim() || !transportEvent?.trim() || !signatureHeader || typeof secret !== "string" || !secret.trim()) {
|
|
126
|
+
fail();
|
|
127
|
+
}
|
|
128
|
+
const toleranceSeconds = options.toleranceSeconds ?? 300;
|
|
129
|
+
const now = options.now ?? Date.now();
|
|
130
|
+
if (!Number.isFinite(toleranceSeconds) || toleranceSeconds < 0 || !Number.isFinite(now)) fail();
|
|
131
|
+
const { timestamp, signatures } = parseSignature(signatureHeader);
|
|
132
|
+
if (Math.abs(now / 1e3 - timestamp) > toleranceSeconds) fail();
|
|
133
|
+
const body = decodeJson(rawBody);
|
|
134
|
+
if (typeof body !== "object" || body === null || typeof body.event !== "string") fail();
|
|
135
|
+
if (!body.event.trim() || body.event !== transportEvent) fail();
|
|
136
|
+
const key = await crypto.subtle.importKey(
|
|
137
|
+
"raw",
|
|
138
|
+
new TextEncoder().encode(secret),
|
|
139
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
140
|
+
false,
|
|
141
|
+
["sign"]
|
|
142
|
+
);
|
|
143
|
+
const mac = new Uint8Array(
|
|
144
|
+
await crypto.subtle.sign(
|
|
145
|
+
"HMAC",
|
|
146
|
+
key,
|
|
147
|
+
Uint8Array.from(signedBytes(timestamp, rawBody)).buffer
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
let verified = false;
|
|
151
|
+
for (const signature of signatures) {
|
|
152
|
+
verified = constantTimeEqual(mac, signature) || verified;
|
|
153
|
+
}
|
|
154
|
+
if (!verified) fail();
|
|
155
|
+
const bodyHash = new Uint8Array(
|
|
156
|
+
await crypto.subtle.digest(
|
|
157
|
+
"SHA-256",
|
|
158
|
+
Uint8Array.from(bodyBytes(rawBody)).buffer
|
|
159
|
+
)
|
|
160
|
+
);
|
|
161
|
+
return {
|
|
162
|
+
transportId: id,
|
|
163
|
+
dedupeKey: `body-sha256:${encodeHex(bodyHash)}`,
|
|
164
|
+
event: body.event,
|
|
165
|
+
timestamp,
|
|
166
|
+
body
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/validators.ts
|
|
171
|
+
function invalid() {
|
|
172
|
+
throw new PickleballInvalidResponseError();
|
|
173
|
+
}
|
|
174
|
+
function record(value) {
|
|
175
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) invalid();
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
function nonblank(value) {
|
|
179
|
+
if (typeof value !== "string" || value.trim() === "") invalid();
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
function finite(value) {
|
|
183
|
+
if (typeof value !== "number" || !Number.isFinite(value)) invalid();
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
function nullableString(value) {
|
|
187
|
+
if (value === null) return null;
|
|
188
|
+
return nonblank(value);
|
|
189
|
+
}
|
|
190
|
+
function nullableNumber(value) {
|
|
191
|
+
if (value === null) return null;
|
|
192
|
+
return finite(value);
|
|
193
|
+
}
|
|
194
|
+
function validateRemoteConfig(value) {
|
|
195
|
+
const data = record(value);
|
|
196
|
+
if (typeof data.enabled !== "boolean" || data.protocolVersion !== 1 || data.videoQuality !== 720 && data.videoQuality !== 1080) {
|
|
197
|
+
invalid();
|
|
198
|
+
}
|
|
199
|
+
const reconnectTimeoutMs = finite(data.reconnectTimeoutMs);
|
|
200
|
+
const backgroundGraceMs = finite(data.backgroundGraceMs);
|
|
201
|
+
const telemetryIntervalMs = finite(data.telemetryIntervalMs);
|
|
202
|
+
const maxSessionDurationMs = finite(data.maxSessionDurationMs);
|
|
203
|
+
if (reconnectTimeoutMs < 0 || backgroundGraceMs < 0 || telemetryIntervalMs <= 0 || maxSessionDurationMs <= 0) {
|
|
204
|
+
invalid();
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
enabled: data.enabled,
|
|
208
|
+
protocolVersion: 1,
|
|
209
|
+
minSdkVersion: nonblank(data.minSdkVersion),
|
|
210
|
+
recommendedSdkVersion: nonblank(data.recommendedSdkVersion),
|
|
211
|
+
videoQuality: data.videoQuality,
|
|
212
|
+
reconnectTimeoutMs,
|
|
213
|
+
backgroundGraceMs,
|
|
214
|
+
telemetryIntervalMs,
|
|
215
|
+
maxSessionDurationMs,
|
|
216
|
+
...data.standbyTimeoutMs === void 0 ? {} : { standbyTimeoutMs: finite(data.standbyTimeoutMs) }
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function validateBootstrap(value) {
|
|
220
|
+
const data = record(value);
|
|
221
|
+
if (typeof data.rolloutEnabled !== "boolean") invalid();
|
|
222
|
+
if (data.message !== void 0 && typeof data.message !== "string") invalid();
|
|
223
|
+
return {
|
|
224
|
+
config: validateRemoteConfig(data.config),
|
|
225
|
+
serverTime: finite(data.serverTime),
|
|
226
|
+
rolloutEnabled: data.rolloutEnabled,
|
|
227
|
+
...data.message === void 0 ? {} : { message: data.message }
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function validateTelemetry(value) {
|
|
231
|
+
const data = record(value);
|
|
232
|
+
return {
|
|
233
|
+
endpoint: nonblank(data.endpoint),
|
|
234
|
+
token: nonblank(data.token),
|
|
235
|
+
expiresAt: finite(data.expiresAt)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function validateGrant(value) {
|
|
239
|
+
const data = record(value);
|
|
240
|
+
return {
|
|
241
|
+
sessionId: nonblank(data.sessionId),
|
|
242
|
+
serverUrl: nonblank(data.serverUrl),
|
|
243
|
+
participantToken: nonblank(data.participantToken),
|
|
244
|
+
playbackUrl: nullableString(data.playbackUrl),
|
|
245
|
+
tokenExpiresAt: finite(data.tokenExpiresAt),
|
|
246
|
+
telemetry: validateTelemetry(data.telemetry),
|
|
247
|
+
config: validateRemoteConfig(data.config)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function validateRefresh(value) {
|
|
251
|
+
const data = record(value);
|
|
252
|
+
return {
|
|
253
|
+
...data.participantToken === void 0 ? {} : { participantToken: nonblank(data.participantToken) },
|
|
254
|
+
...data.tokenExpiresAt === void 0 ? {} : { tokenExpiresAt: finite(data.tokenExpiresAt) },
|
|
255
|
+
...data.telemetry === void 0 ? {} : { telemetry: validateTelemetry(data.telemetry) },
|
|
256
|
+
config: validateRemoteConfig(data.config)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function validateEnd(value, sessionId) {
|
|
260
|
+
const data = record(value);
|
|
261
|
+
if (data.sessionId !== sessionId || data.status !== "ended") invalid();
|
|
262
|
+
}
|
|
263
|
+
function validatePublishState(value, sessionId) {
|
|
264
|
+
const data = record(value);
|
|
265
|
+
if (data.sessionId !== sessionId) invalid();
|
|
266
|
+
if (data.status !== "scheduled" && data.status !== "live") invalid();
|
|
267
|
+
return { sessionId, status: data.status };
|
|
268
|
+
}
|
|
269
|
+
function validateSession(value) {
|
|
270
|
+
const data = record(value);
|
|
271
|
+
if (data.status !== "scheduled" && data.status !== "live" && data.status !== "ended") {
|
|
272
|
+
invalid();
|
|
273
|
+
}
|
|
274
|
+
if (data.visibility !== "public" && data.visibility !== "private") invalid();
|
|
275
|
+
const viewerCount = finite(data.viewerCount);
|
|
276
|
+
const likeCount = finite(data.likeCount);
|
|
277
|
+
const shareCount = finite(data.shareCount);
|
|
278
|
+
if (viewerCount < 0 || likeCount < 0 || shareCount < 0) invalid();
|
|
279
|
+
return {
|
|
280
|
+
id: nonblank(data.id),
|
|
281
|
+
title: nonblank(data.title),
|
|
282
|
+
status: data.status,
|
|
283
|
+
visibility: data.visibility,
|
|
284
|
+
matchRef: nullableString(data.matchRef),
|
|
285
|
+
playbackUrl: nullableString(data.playbackUrl),
|
|
286
|
+
watchUrl: nullableString(data.watchUrl),
|
|
287
|
+
scheduledAt: nullableNumber(data.scheduledAt),
|
|
288
|
+
startedAt: nullableNumber(data.startedAt),
|
|
289
|
+
endedAt: nullableNumber(data.endedAt),
|
|
290
|
+
viewerCount,
|
|
291
|
+
likeCount,
|
|
292
|
+
shareCount
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function validateRecording(value, sessionId) {
|
|
296
|
+
const data = record(value);
|
|
297
|
+
if (data.sessionId !== sessionId) invalid();
|
|
298
|
+
if (data.type !== "clean" && data.type !== "derived" && data.type !== "device") invalid();
|
|
299
|
+
if (data.status !== "recording" && data.status !== "ready" && data.status !== "failed") {
|
|
300
|
+
invalid();
|
|
301
|
+
}
|
|
302
|
+
const durationSec = nullableNumber(data.durationSec);
|
|
303
|
+
if (durationSec !== null && durationSec < 0) invalid();
|
|
304
|
+
return {
|
|
305
|
+
id: nonblank(data.id),
|
|
306
|
+
sessionId,
|
|
307
|
+
type: data.type,
|
|
308
|
+
status: data.status,
|
|
309
|
+
url: nullableString(data.url),
|
|
310
|
+
durationSec
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function validateRecordings(value, sessionId) {
|
|
314
|
+
if (!Array.isArray(value)) invalid();
|
|
315
|
+
return value.map((item) => validateRecording(item, sessionId));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/contracts.ts
|
|
319
|
+
var SDK_TELEMETRY_EVENT_NAMES = [
|
|
320
|
+
"state_changed",
|
|
321
|
+
"connection_quality",
|
|
322
|
+
"reconnect",
|
|
323
|
+
"error",
|
|
324
|
+
"heartbeat",
|
|
325
|
+
"session_ended"
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
// src/index.ts
|
|
329
|
+
var TRANSIENT_STATUSES = /* @__PURE__ */ new Set([408, 429]);
|
|
330
|
+
var RETRY_BASE_MS = 100;
|
|
331
|
+
var RETRY_MAX_MS = 2e3;
|
|
332
|
+
var RETRY_AFTER_MAX_MS = 3e4;
|
|
333
|
+
var RESPONSE_BODY_MAX_BYTES = 1024 * 1024;
|
|
334
|
+
var SDK_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
335
|
+
"PERMISSION_DENIED",
|
|
336
|
+
"NETWORK_UNAVAILABLE",
|
|
337
|
+
"TOKEN_EXPIRED",
|
|
338
|
+
"SDK_UPGRADE_REQUIRED",
|
|
339
|
+
"SESSION_CONFLICT",
|
|
340
|
+
"RECORDING_FAILED",
|
|
341
|
+
"CONSENT_REQUIRED",
|
|
342
|
+
"SDK_DISABLED",
|
|
343
|
+
"UNKNOWN"
|
|
344
|
+
]);
|
|
345
|
+
var DETERMINISTIC_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
346
|
+
"PERMISSION_DENIED",
|
|
347
|
+
"TOKEN_EXPIRED",
|
|
348
|
+
"SDK_UPGRADE_REQUIRED",
|
|
349
|
+
"SESSION_CONFLICT",
|
|
350
|
+
"RECORDING_FAILED",
|
|
351
|
+
"CONSENT_REQUIRED",
|
|
352
|
+
"SDK_DISABLED"
|
|
353
|
+
]);
|
|
354
|
+
function requiredNonblank(value, name) {
|
|
355
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
356
|
+
throw new PickleballConfigurationError(`${name} must be a non-empty string`);
|
|
357
|
+
}
|
|
358
|
+
return value;
|
|
359
|
+
}
|
|
360
|
+
function validatedBaseUrl(value) {
|
|
361
|
+
let url;
|
|
362
|
+
try {
|
|
363
|
+
url = new URL(value);
|
|
364
|
+
} catch {
|
|
365
|
+
throw new PickleballConfigurationError("baseUrl must be an absolute HTTP(S) URL");
|
|
366
|
+
}
|
|
367
|
+
const loopbackHosts = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
368
|
+
const secure = url.protocol === "https:";
|
|
369
|
+
const localDevelopment = url.protocol === "http:" && loopbackHosts.has(url.hostname);
|
|
370
|
+
if (!secure && !localDevelopment || url.username || url.password) {
|
|
371
|
+
throw new PickleballConfigurationError(
|
|
372
|
+
"baseUrl must use HTTPS (HTTP is allowed only for localhost loopback)"
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
376
|
+
url.search = "";
|
|
377
|
+
url.hash = "";
|
|
378
|
+
return url.toString().replace(/\/$/, "");
|
|
379
|
+
}
|
|
380
|
+
function positiveFinite(value, name) {
|
|
381
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
382
|
+
throw new PickleballConfigurationError(`${name} must be a positive finite number`);
|
|
383
|
+
}
|
|
384
|
+
return value;
|
|
385
|
+
}
|
|
386
|
+
function validRetryCount(value) {
|
|
387
|
+
if (!Number.isInteger(value) || value < 0 || value > 10) {
|
|
388
|
+
throw new PickleballConfigurationError("maxRetries must be an integer between 0 and 10");
|
|
389
|
+
}
|
|
390
|
+
return value;
|
|
391
|
+
}
|
|
392
|
+
function isTransientStatus(status) {
|
|
393
|
+
return TRANSIENT_STATUSES.has(status) || status >= 500;
|
|
394
|
+
}
|
|
395
|
+
function isRetryableResponseError(error) {
|
|
396
|
+
return !(error instanceof PickleballApiError && DETERMINISTIC_ERROR_CODES.has(error.code));
|
|
397
|
+
}
|
|
398
|
+
function retryAfterMs(response, now = Date.now()) {
|
|
399
|
+
const raw = response.headers.get("retry-after");
|
|
400
|
+
if (raw === null) return null;
|
|
401
|
+
const seconds = Number(raw);
|
|
402
|
+
const delay = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(raw) - now;
|
|
403
|
+
if (!Number.isFinite(delay) || delay < 0) return null;
|
|
404
|
+
return Math.min(delay, RETRY_AFTER_MAX_MS);
|
|
405
|
+
}
|
|
406
|
+
function wait(ms) {
|
|
407
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
408
|
+
}
|
|
409
|
+
function isRecord(value) {
|
|
410
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
411
|
+
}
|
|
412
|
+
function parseSdkError(payload, status, apiKey) {
|
|
413
|
+
if (!isRecord(payload) || !("error" in payload)) {
|
|
414
|
+
return new PickleballInvalidResponseError();
|
|
415
|
+
}
|
|
416
|
+
if (typeof payload.error === "string") {
|
|
417
|
+
return new PickleballHttpError(status);
|
|
418
|
+
}
|
|
419
|
+
if (!isRecord(payload.error)) {
|
|
420
|
+
return new PickleballInvalidResponseError();
|
|
421
|
+
}
|
|
422
|
+
const { code, message } = payload.error;
|
|
423
|
+
if (typeof code !== "string" || code.trim() === "" || typeof message !== "string") {
|
|
424
|
+
return new PickleballInvalidResponseError();
|
|
425
|
+
}
|
|
426
|
+
const safeMessage = apiKey === "" ? message : message.split(apiKey).join("[REDACTED]");
|
|
427
|
+
const safeCode = SDK_ERROR_CODES.has(code) ? code : "UNKNOWN";
|
|
428
|
+
return new PickleballApiError(safeMessage, safeCode, status);
|
|
429
|
+
}
|
|
430
|
+
async function readBoundedResponseText(response) {
|
|
431
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
432
|
+
if (Number.isFinite(declaredLength) && declaredLength > RESPONSE_BODY_MAX_BYTES) {
|
|
433
|
+
await response.body?.cancel().catch(() => void 0);
|
|
434
|
+
throw new PickleballInvalidResponseError();
|
|
435
|
+
}
|
|
436
|
+
if (!response.body) return "";
|
|
437
|
+
const reader = response.body.getReader();
|
|
438
|
+
const chunks = [];
|
|
439
|
+
let total = 0;
|
|
440
|
+
try {
|
|
441
|
+
while (true) {
|
|
442
|
+
const { done, value } = await reader.read();
|
|
443
|
+
if (done) break;
|
|
444
|
+
total += value.byteLength;
|
|
445
|
+
if (total > RESPONSE_BODY_MAX_BYTES) {
|
|
446
|
+
await reader.cancel().catch(() => void 0);
|
|
447
|
+
throw new PickleballInvalidResponseError();
|
|
448
|
+
}
|
|
449
|
+
chunks.push(value);
|
|
450
|
+
}
|
|
451
|
+
} finally {
|
|
452
|
+
reader.releaseLock();
|
|
453
|
+
}
|
|
454
|
+
const bytes = new Uint8Array(total);
|
|
455
|
+
let offset = 0;
|
|
456
|
+
for (const chunk of chunks) {
|
|
457
|
+
bytes.set(chunk, offset);
|
|
458
|
+
offset += chunk.byteLength;
|
|
459
|
+
}
|
|
460
|
+
return new TextDecoder().decode(bytes);
|
|
461
|
+
}
|
|
462
|
+
async function decodeResponse(response, apiKey, validate) {
|
|
463
|
+
if (response.status >= 300 && response.status < 400) {
|
|
464
|
+
await response.body?.cancel().catch(() => void 0);
|
|
465
|
+
throw new PickleballHttpError(response.status);
|
|
466
|
+
}
|
|
467
|
+
const text = await readBoundedResponseText(response);
|
|
468
|
+
let payload;
|
|
469
|
+
try {
|
|
470
|
+
payload = JSON.parse(text);
|
|
471
|
+
} catch {
|
|
472
|
+
throw new PickleballInvalidResponseError();
|
|
473
|
+
}
|
|
474
|
+
if (!response.ok) throw parseSdkError(payload, response.status, apiKey);
|
|
475
|
+
if (!isRecord(payload) || !("data" in payload)) {
|
|
476
|
+
throw new PickleballInvalidResponseError();
|
|
477
|
+
}
|
|
478
|
+
return validate(payload.data);
|
|
479
|
+
}
|
|
480
|
+
function createPickleballLiveClient(options) {
|
|
481
|
+
const baseUrl = validatedBaseUrl(options.baseUrl);
|
|
482
|
+
const appId = requiredNonblank(options.appId, "appId");
|
|
483
|
+
const apiKey = requiredNonblank(options.apiKey, "apiKey");
|
|
484
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
485
|
+
if (typeof fetchImplementation !== "function") {
|
|
486
|
+
throw new PickleballConfigurationError("fetch must be available");
|
|
487
|
+
}
|
|
488
|
+
const timeoutMs = positiveFinite(options.timeoutMs ?? 1e4, "timeoutMs");
|
|
489
|
+
const maxRetries = validRetryCount(options.maxRetries ?? 2);
|
|
490
|
+
async function request({
|
|
491
|
+
method,
|
|
492
|
+
path,
|
|
493
|
+
body,
|
|
494
|
+
retryPolicy,
|
|
495
|
+
validate
|
|
496
|
+
}) {
|
|
497
|
+
const retryLimit = retryPolicy === "transient" ? maxRetries : 0;
|
|
498
|
+
for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
|
|
499
|
+
const controller = new AbortController();
|
|
500
|
+
let response;
|
|
501
|
+
let timedOut = false;
|
|
502
|
+
const timer = setTimeout(() => {
|
|
503
|
+
timedOut = true;
|
|
504
|
+
controller.abort();
|
|
505
|
+
}, timeoutMs);
|
|
506
|
+
try {
|
|
507
|
+
response = await fetchImplementation(`${baseUrl}${path}`, {
|
|
508
|
+
method,
|
|
509
|
+
headers: method === "POST" ? {
|
|
510
|
+
accept: "application/json",
|
|
511
|
+
"content-type": "application/json",
|
|
512
|
+
"x-api-key": apiKey
|
|
513
|
+
} : {
|
|
514
|
+
accept: "application/json",
|
|
515
|
+
"x-api-key": apiKey,
|
|
516
|
+
"x-pickleball-app-id": appId
|
|
517
|
+
},
|
|
518
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
519
|
+
signal: controller.signal,
|
|
520
|
+
redirect: "error"
|
|
521
|
+
});
|
|
522
|
+
try {
|
|
523
|
+
return await decodeResponse(response, apiKey, validate);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (isTransientStatus(response.status) && attempt < retryLimit && isRetryableResponseError(error)) {
|
|
526
|
+
clearTimeout(timer);
|
|
527
|
+
const delay = retryAfterMs(response) ?? Math.min(RETRY_BASE_MS * 2 ** attempt, RETRY_MAX_MS);
|
|
528
|
+
await response.body?.cancel().catch(() => void 0);
|
|
529
|
+
await wait(delay);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
} catch (error) {
|
|
535
|
+
if (error instanceof PickleballApiError || error instanceof PickleballHttpError || error instanceof PickleballInvalidResponseError) {
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
if (attempt < retryLimit) {
|
|
539
|
+
await wait(Math.min(RETRY_BASE_MS * 2 ** attempt, RETRY_MAX_MS));
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (timedOut) throw new PickleballTimeoutError();
|
|
543
|
+
throw new PickleballNetworkError();
|
|
544
|
+
} finally {
|
|
545
|
+
clearTimeout(timer);
|
|
546
|
+
if (timedOut) await response?.body?.cancel().catch(() => void 0);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
throw new PickleballNetworkError();
|
|
550
|
+
}
|
|
551
|
+
const post = (path, body, retryPolicy, validate) => request({ method: "POST", path, body, retryPolicy, validate });
|
|
552
|
+
const get = (path, validate) => request({ method: "GET", path, retryPolicy: "transient", validate });
|
|
553
|
+
const sessionPath = (sessionId) => `/api/v2/sdk/sessions/${encodeURIComponent(sessionId)}`;
|
|
554
|
+
return {
|
|
555
|
+
apps: {
|
|
556
|
+
bootstrap: (input) => post(
|
|
557
|
+
"/api/v2/sdk/bootstrap",
|
|
558
|
+
{ ...input, appId },
|
|
559
|
+
"transient",
|
|
560
|
+
validateBootstrap
|
|
561
|
+
)
|
|
562
|
+
},
|
|
563
|
+
sessions: {
|
|
564
|
+
start: (input) => post(
|
|
565
|
+
"/api/v2/sdk/sessions",
|
|
566
|
+
{ ...input, appId },
|
|
567
|
+
"transient",
|
|
568
|
+
validateGrant
|
|
569
|
+
),
|
|
570
|
+
refresh: (sessionId, input) => post(
|
|
571
|
+
`${sessionPath(sessionId)}/refresh`,
|
|
572
|
+
{ ...input, appId },
|
|
573
|
+
"never",
|
|
574
|
+
validateRefresh
|
|
575
|
+
),
|
|
576
|
+
end: async (input) => {
|
|
577
|
+
await post(
|
|
578
|
+
`${sessionPath(input.sessionId)}/end`,
|
|
579
|
+
{ ...input, appId },
|
|
580
|
+
"transient",
|
|
581
|
+
(value) => validateEnd(value, input.sessionId)
|
|
582
|
+
);
|
|
583
|
+
},
|
|
584
|
+
publish: (sessionId) => post(
|
|
585
|
+
`${sessionPath(sessionId)}/publish`,
|
|
586
|
+
{ sessionId, appId },
|
|
587
|
+
"transient",
|
|
588
|
+
(value) => validatePublishState(value, sessionId)
|
|
589
|
+
),
|
|
590
|
+
unpublish: (sessionId) => post(
|
|
591
|
+
`${sessionPath(sessionId)}/unpublish`,
|
|
592
|
+
{ sessionId, appId },
|
|
593
|
+
"transient",
|
|
594
|
+
(value) => validatePublishState(value, sessionId)
|
|
595
|
+
),
|
|
596
|
+
get: (sessionId) => get(sessionPath(sessionId), validateSession),
|
|
597
|
+
getRecordings: (sessionId) => get(
|
|
598
|
+
`${sessionPath(sessionId)}/recordings`,
|
|
599
|
+
(value) => validateRecordings(value, sessionId)
|
|
600
|
+
)
|
|
601
|
+
},
|
|
602
|
+
webhooks: { verify: verifyWebhook }
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export {
|
|
607
|
+
PickleballLiveError,
|
|
608
|
+
PickleballConfigurationError,
|
|
609
|
+
PickleballApiError,
|
|
610
|
+
PickleballHttpError,
|
|
611
|
+
PickleballInvalidResponseError,
|
|
612
|
+
PickleballTimeoutError,
|
|
613
|
+
PickleballNetworkError,
|
|
614
|
+
PickleballWebhookVerificationError,
|
|
615
|
+
verifyWebhook,
|
|
616
|
+
SDK_TELEMETRY_EVENT_NAMES,
|
|
617
|
+
createPickleballLiveClient
|
|
618
|
+
};
|