@ix-xs/twitch.api 1.0.5 → 2.0.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/index.js ADDED
@@ -0,0 +1,3738 @@
1
+ // src/api/base.ts
2
+ var ApiGroup = class {
3
+ rest;
4
+ constructor(rest) {
5
+ this.rest = rest;
6
+ }
7
+ };
8
+
9
+ // src/api/ads.ts
10
+ var AdsApi = class extends ApiGroup {
11
+ startCommercial(params, options) {
12
+ return this.rest.one({
13
+ method: "POST",
14
+ path: "channels/commercial",
15
+ body: params,
16
+ scopes: ["channel:edit:commercial"],
17
+ context: { body: "broadcaster_id" },
18
+ options
19
+ });
20
+ }
21
+ schedule(params = {}, options) {
22
+ return this.rest.one({
23
+ path: "channels/ads",
24
+ query: params,
25
+ scopes: ["channel:read:ads"],
26
+ context: "broadcaster_id",
27
+ options
28
+ });
29
+ }
30
+ snooze(params = {}, options) {
31
+ return this.rest.one({
32
+ method: "POST",
33
+ path: "channels/ads/schedule/snooze",
34
+ query: params,
35
+ scopes: ["channel:manage:ads"],
36
+ context: "broadcaster_id",
37
+ options
38
+ });
39
+ }
40
+ };
41
+
42
+ // src/api/analytics.ts
43
+ var AnalyticsApi = class extends ApiGroup {
44
+ extensions(params = {}, options) {
45
+ return this.rest.paginate({
46
+ path: "analytics/extensions",
47
+ query: params,
48
+ auth: "user",
49
+ scopes: ["analytics:read:extensions"],
50
+ options
51
+ });
52
+ }
53
+ games(params = {}, options) {
54
+ return this.rest.paginate({
55
+ path: "analytics/games",
56
+ query: params,
57
+ auth: "user",
58
+ scopes: ["analytics:read:games"],
59
+ options
60
+ });
61
+ }
62
+ };
63
+
64
+ // src/api/bits.ts
65
+ var BitsApi = class extends ApiGroup {
66
+ async leaderboard(params = {}, options) {
67
+ const body = await this.rest.request({
68
+ path: "bits/leaderboard",
69
+ query: params,
70
+ auth: "user",
71
+ scopes: ["bits:read"],
72
+ options
73
+ });
74
+ return { data: body.data, date_range: body.date_range, total: body.total };
75
+ }
76
+ cheermotes(params = {}, options) {
77
+ return this.rest.data({ path: "bits/cheermotes", query: params, options });
78
+ }
79
+ customPowerUps(params = {}, options) {
80
+ return this.rest.chunked(
81
+ {
82
+ path: "bits/custom_power_ups",
83
+ query: params,
84
+ auth: "user",
85
+ scopes: ["bits:read"],
86
+ context: "broadcaster_id",
87
+ options
88
+ },
89
+ ["id"],
90
+ 50
91
+ );
92
+ }
93
+ extensionTransactions(params, options) {
94
+ return this.rest.paginate({
95
+ path: "extensions/transactions",
96
+ query: params,
97
+ auth: "app",
98
+ options
99
+ });
100
+ }
101
+ };
102
+
103
+ // src/api/channels.ts
104
+ var ChannelsApi = class extends ApiGroup {
105
+ get(params, options) {
106
+ return this.rest.chunked({ path: "channels", query: params, options }, ["broadcaster_id"]);
107
+ }
108
+ update(params = {}, options) {
109
+ const { broadcaster_id, ...body } = params;
110
+ return this.rest.send({
111
+ method: "PATCH",
112
+ path: "channels",
113
+ query: { broadcaster_id },
114
+ body,
115
+ auth: "user",
116
+ scopes: ["channel:manage:broadcast"],
117
+ context: "broadcaster_id",
118
+ options
119
+ });
120
+ }
121
+ editors(params = {}, options) {
122
+ return this.rest.data({
123
+ path: "channels/editors",
124
+ query: params,
125
+ auth: "user",
126
+ scopes: ["channel:read:editors"],
127
+ context: "broadcaster_id",
128
+ options
129
+ });
130
+ }
131
+ followed(params = {}, options) {
132
+ return this.rest.paginate({
133
+ path: "channels/followed",
134
+ query: params,
135
+ auth: "user",
136
+ scopes: ["user:read:follows"],
137
+ context: "user_id",
138
+ options
139
+ });
140
+ }
141
+ followers(params, options) {
142
+ return this.rest.paginate({ path: "channels/followers", query: params, prefer: "user", options });
143
+ }
144
+ };
145
+
146
+ // src/api/channel-points.ts
147
+ var ChannelPointsApi = class extends ApiGroup {
148
+ createReward(params, options) {
149
+ const { broadcaster_id, ...body } = params;
150
+ return this.rest.one({
151
+ method: "POST",
152
+ path: "channel_points/custom_rewards",
153
+ query: { broadcaster_id },
154
+ body,
155
+ auth: "user",
156
+ scopes: ["channel:manage:redemptions"],
157
+ context: "broadcaster_id",
158
+ options
159
+ });
160
+ }
161
+ deleteReward(params, options) {
162
+ return this.rest.send({
163
+ method: "DELETE",
164
+ path: "channel_points/custom_rewards",
165
+ query: params,
166
+ auth: "user",
167
+ scopes: ["channel:manage:redemptions"],
168
+ context: "broadcaster_id",
169
+ options
170
+ });
171
+ }
172
+ rewards(params = {}, options) {
173
+ return this.rest.chunked(
174
+ {
175
+ path: "channel_points/custom_rewards",
176
+ query: params,
177
+ auth: "user",
178
+ scopes: ["channel:read:redemptions", "channel:manage:redemptions"],
179
+ context: "broadcaster_id",
180
+ options
181
+ },
182
+ ["id"],
183
+ 50
184
+ );
185
+ }
186
+ redemptions(params, options) {
187
+ return this.rest.paginate(
188
+ {
189
+ path: "channel_points/custom_rewards/redemptions",
190
+ query: params,
191
+ auth: "user",
192
+ scopes: ["channel:read:redemptions", "channel:manage:redemptions"],
193
+ context: "broadcaster_id",
194
+ options
195
+ },
196
+ { max: 50 }
197
+ );
198
+ }
199
+ updateReward(params, options) {
200
+ const { broadcaster_id, id, ...body } = params;
201
+ return this.rest.one({
202
+ method: "PATCH",
203
+ path: "channel_points/custom_rewards",
204
+ query: { broadcaster_id, id },
205
+ body,
206
+ auth: "user",
207
+ scopes: ["channel:manage:redemptions"],
208
+ context: "broadcaster_id",
209
+ options
210
+ });
211
+ }
212
+ updateRedemptions(params, options) {
213
+ const { status, ...query } = params;
214
+ return this.rest.data({
215
+ method: "PATCH",
216
+ path: "channel_points/custom_rewards/redemptions",
217
+ query,
218
+ body: { status },
219
+ auth: "user",
220
+ scopes: ["channel:manage:redemptions"],
221
+ context: "broadcaster_id",
222
+ options
223
+ });
224
+ }
225
+ };
226
+
227
+ // src/api/charity.ts
228
+ var CharityApi = class extends ApiGroup {
229
+ campaign(params = {}, options) {
230
+ return this.rest.first({
231
+ path: "charity/campaigns",
232
+ query: params,
233
+ auth: "user",
234
+ scopes: ["channel:read:charity"],
235
+ context: "broadcaster_id",
236
+ options
237
+ });
238
+ }
239
+ donations(params = {}, options) {
240
+ return this.rest.paginate({
241
+ path: "charity/donations",
242
+ query: params,
243
+ auth: "user",
244
+ scopes: ["channel:read:charity"],
245
+ context: "broadcaster_id",
246
+ options
247
+ });
248
+ }
249
+ };
250
+
251
+ // src/util.ts
252
+ var Emitter = class {
253
+ #listeners = /* @__PURE__ */ new Map();
254
+ on(event, listener) {
255
+ let set = this.#listeners.get(event);
256
+ if (!set) this.#listeners.set(event, set = /* @__PURE__ */ new Set());
257
+ set.add(listener);
258
+ return () => this.off(event, listener);
259
+ }
260
+ once(event, listener) {
261
+ const off = this.on(event, ((...args) => {
262
+ off();
263
+ listener(...args);
264
+ }));
265
+ return off;
266
+ }
267
+ off(event, listener) {
268
+ this.#listeners.get(event)?.delete(listener);
269
+ }
270
+ emit(event, ...args) {
271
+ const set = this.#listeners.get(event);
272
+ if (!set || set.size === 0) return false;
273
+ for (const listener of [...set]) {
274
+ try {
275
+ listener(...args);
276
+ } catch (error) {
277
+ queueMicrotask(() => {
278
+ throw error;
279
+ });
280
+ }
281
+ }
282
+ return true;
283
+ }
284
+ has(event) {
285
+ return (this.#listeners.get(event)?.size ?? 0) > 0;
286
+ }
287
+ clear() {
288
+ this.#listeners.clear();
289
+ }
290
+ };
291
+ function sleep(ms, signal) {
292
+ return new Promise((resolve, reject) => {
293
+ if (signal?.aborted) return reject(signal.reason);
294
+ const timer = setTimeout(() => {
295
+ signal?.removeEventListener("abort", onAbort);
296
+ resolve();
297
+ }, ms);
298
+ const onAbort = () => {
299
+ clearTimeout(timer);
300
+ reject(signal.reason);
301
+ };
302
+ signal?.addEventListener("abort", onAbort, { once: true });
303
+ });
304
+ }
305
+ function unref(timer) {
306
+ if (typeof timer === "number") globalThis.Deno?.unrefTimer?.(timer);
307
+ else timer.unref?.();
308
+ return timer;
309
+ }
310
+ function backoff(attempt, base = 250, max = 8e3) {
311
+ const ceiling = Math.min(max, base * 2 ** attempt);
312
+ return Math.round(ceiling / 2 + Math.random() * (ceiling / 2));
313
+ }
314
+ function fingerprint(text) {
315
+ let hash = 2166136261;
316
+ for (let i = 0; i < text.length; i++) {
317
+ hash ^= text.charCodeAt(i);
318
+ hash = Math.imul(hash, 16777619);
319
+ }
320
+ return (hash >>> 0).toString(16).padStart(8, "0");
321
+ }
322
+ function chunk(items, size) {
323
+ const out = [];
324
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
325
+ return out;
326
+ }
327
+ function toArray(value) {
328
+ if (value === void 0) return [];
329
+ return Array.isArray(value) ? [...value] : [value];
330
+ }
331
+ var BoundedMap = class {
332
+ #map = /* @__PURE__ */ new Map();
333
+ #max;
334
+ constructor(max) {
335
+ this.#max = max;
336
+ }
337
+ get size() {
338
+ return this.#map.size;
339
+ }
340
+ has(key) {
341
+ return this.#map.has(key);
342
+ }
343
+ get(key) {
344
+ return this.#map.get(key);
345
+ }
346
+ set(key, value) {
347
+ if (this.#map.has(key)) this.#map.delete(key);
348
+ else if (this.#map.size >= this.#max) {
349
+ const oldest = this.#map.keys().next();
350
+ if (!oldest.done) this.#map.delete(oldest.value);
351
+ }
352
+ this.#map.set(key, value);
353
+ }
354
+ delete(key) {
355
+ return this.#map.delete(key);
356
+ }
357
+ clear() {
358
+ this.#map.clear();
359
+ }
360
+ };
361
+
362
+ // src/api/chat.ts
363
+ var ChatApi = class extends ApiGroup {
364
+ chatters(params, options) {
365
+ return this.rest.paginate(
366
+ {
367
+ path: "chat/chatters",
368
+ query: params,
369
+ scopes: ["moderator:read:chatters"],
370
+ context: "moderator_id",
371
+ options
372
+ },
373
+ { max: 1e3 }
374
+ );
375
+ }
376
+ async channelEmotes(params, options) {
377
+ const { data, template } = await this.rest.request({
378
+ path: "chat/emotes",
379
+ query: params,
380
+ options
381
+ });
382
+ return { data, template };
383
+ }
384
+ async globalEmotes(options) {
385
+ const { data, template } = await this.rest.request({
386
+ path: "chat/emotes/global",
387
+ options
388
+ });
389
+ return { data, template };
390
+ }
391
+ async emoteSets(params, options) {
392
+ const ids = typeof params.emote_set_id === "string" ? [params.emote_set_id] : params.emote_set_id;
393
+ const bodies = await Promise.all(
394
+ chunk(ids, 25).map(
395
+ (emote_set_id) => this.rest.request({ path: "chat/emotes/set", query: { emote_set_id }, options })
396
+ )
397
+ );
398
+ return { data: bodies.flatMap((body) => body.data), template: bodies[0]?.template ?? "" };
399
+ }
400
+ channelBadges(params, options) {
401
+ return this.rest.data({ path: "chat/badges", query: params, options });
402
+ }
403
+ globalBadges(options) {
404
+ return this.rest.data({ path: "chat/badges/global", options });
405
+ }
406
+ settings(params, options) {
407
+ return this.rest.one({ path: "chat/settings", query: params, options });
408
+ }
409
+ sharedSession(params, options) {
410
+ return this.rest.first({ path: "shared_chat/session", query: params, options });
411
+ }
412
+ userEmotes(params = {}, options) {
413
+ return this.rest.paginate(
414
+ {
415
+ path: "chat/emotes/user",
416
+ query: params,
417
+ auth: "user",
418
+ scopes: ["user:read:emotes"],
419
+ context: "user_id",
420
+ options
421
+ },
422
+ // The endpoint has no page size parameter.
423
+ { sizeParam: null }
424
+ );
425
+ }
426
+ updateSettings(params, options) {
427
+ const { broadcaster_id, moderator_id, ...body } = params;
428
+ return this.rest.one({
429
+ method: "PATCH",
430
+ path: "chat/settings",
431
+ query: { broadcaster_id, moderator_id },
432
+ body,
433
+ scopes: ["moderator:manage:chat_settings"],
434
+ context: "moderator_id",
435
+ options
436
+ });
437
+ }
438
+ announce(params, options) {
439
+ const { broadcaster_id, moderator_id, ...body } = params;
440
+ return this.rest.send({
441
+ method: "POST",
442
+ path: "chat/announcements",
443
+ query: { broadcaster_id, moderator_id },
444
+ body,
445
+ scopes: ["moderator:manage:announcements"],
446
+ context: "moderator_id",
447
+ options
448
+ });
449
+ }
450
+ shoutout(params, options) {
451
+ return this.rest.send({
452
+ method: "POST",
453
+ path: "chat/shoutouts",
454
+ query: params,
455
+ scopes: ["moderator:manage:shoutouts"],
456
+ context: "moderator_id",
457
+ options
458
+ });
459
+ }
460
+ send(params, options) {
461
+ return this.rest.one({
462
+ method: "POST",
463
+ path: "chat/messages",
464
+ body: params,
465
+ scopes: ["user:write:chat"],
466
+ context: { body: "sender_id" },
467
+ options
468
+ });
469
+ }
470
+ pinnedMessage(params, options) {
471
+ return this.rest.first({
472
+ path: "chat/pins",
473
+ query: params,
474
+ scopes: ["moderator:manage:chat_messages", "moderator:read:chat_messages"],
475
+ context: "moderator_id",
476
+ options
477
+ });
478
+ }
479
+ pin(params, options) {
480
+ return this.rest.send({
481
+ method: "PUT",
482
+ path: "chat/pins",
483
+ query: params,
484
+ scopes: ["moderator:manage:chat_messages"],
485
+ context: "moderator_id",
486
+ options
487
+ });
488
+ }
489
+ updatePin(params, options) {
490
+ return this.rest.send({
491
+ method: "PATCH",
492
+ path: "chat/pins",
493
+ query: params,
494
+ scopes: ["moderator:manage:chat_messages"],
495
+ context: "moderator_id",
496
+ options
497
+ });
498
+ }
499
+ unpin(params, options) {
500
+ return this.rest.send({
501
+ method: "DELETE",
502
+ path: "chat/pins",
503
+ query: params,
504
+ scopes: ["moderator:manage:chat_messages"],
505
+ context: "moderator_id",
506
+ options
507
+ });
508
+ }
509
+ colors(params, options) {
510
+ return this.rest.chunked({ path: "chat/color", query: params, options }, ["user_id"]);
511
+ }
512
+ setColor(params, options) {
513
+ return this.rest.send({
514
+ method: "PUT",
515
+ path: "chat/color",
516
+ query: params,
517
+ auth: "user",
518
+ scopes: ["user:manage:chat_color"],
519
+ context: "user_id",
520
+ options
521
+ });
522
+ }
523
+ };
524
+
525
+ // src/api/clips.ts
526
+ var ClipsApi = class extends ApiGroup {
527
+ create(params, options) {
528
+ return this.rest.one({
529
+ method: "POST",
530
+ path: "clips",
531
+ query: params,
532
+ auth: "user",
533
+ scopes: ["clips:edit"],
534
+ options
535
+ });
536
+ }
537
+ createFromVod(params, options) {
538
+ return this.rest.one({
539
+ method: "POST",
540
+ path: "videos/clips",
541
+ query: params,
542
+ scopes: ["editor:manage:clips", "channel:manage:clips"],
543
+ context: "editor_id",
544
+ options
545
+ });
546
+ }
547
+ get(params, options) {
548
+ return this.rest.paginate({ path: "clips", query: params, options });
549
+ }
550
+ downloads(params, options) {
551
+ return this.rest.chunked(
552
+ {
553
+ path: "clips/downloads",
554
+ query: params,
555
+ scopes: ["editor:manage:clips", "channel:manage:clips"],
556
+ context: "editor_id",
557
+ options
558
+ },
559
+ ["clip_id"],
560
+ 10
561
+ );
562
+ }
563
+ };
564
+
565
+ // src/api/conduits.ts
566
+ var ConduitsApi = class extends ApiGroup {
567
+ list(options) {
568
+ return this.rest.data({ path: "eventsub/conduits", auth: "app", options });
569
+ }
570
+ create(params, options) {
571
+ return this.rest.one({ method: "POST", path: "eventsub/conduits", body: params, auth: "app", options });
572
+ }
573
+ update(params, options) {
574
+ return this.rest.one({ method: "PATCH", path: "eventsub/conduits", body: params, auth: "app", options });
575
+ }
576
+ delete(id, options) {
577
+ return this.rest.send({ method: "DELETE", path: "eventsub/conduits", query: { id }, auth: "app", options });
578
+ }
579
+ shards(params, options) {
580
+ return this.rest.paginate(
581
+ { path: "eventsub/conduits/shards", query: params, auth: "app", options },
582
+ // The endpoint has no page size parameter.
583
+ { sizeParam: null }
584
+ );
585
+ }
586
+ async updateShards(params, options) {
587
+ const result = { data: [], errors: [] };
588
+ for (let start = 0; start < params.shards.length; start += 100) {
589
+ const body = await this.rest.request({
590
+ method: "PATCH",
591
+ path: "eventsub/conduits/shards",
592
+ body: { conduit_id: params.conduit_id, shards: params.shards.slice(start, start + 100) },
593
+ auth: "app",
594
+ options
595
+ });
596
+ result.data.push(...body?.data ?? []);
597
+ result.errors.push(...body?.errors ?? []);
598
+ }
599
+ return result;
600
+ }
601
+ };
602
+
603
+ // src/api/content-labels.ts
604
+ var ContentLabelsApi = class extends ApiGroup {
605
+ list(params = {}, options) {
606
+ return this.rest.data({ path: "content_classification_labels", query: params, options });
607
+ }
608
+ };
609
+
610
+ // src/api/entitlements.ts
611
+ var EntitlementsApi = class extends ApiGroup {
612
+ drops(params = {}, options) {
613
+ return this.rest.paginate({ path: "entitlements/drops", query: params, options }, { max: 1e3 });
614
+ }
615
+ updateDrops(params = {}, options) {
616
+ return this.rest.data({
617
+ method: "PATCH",
618
+ path: "entitlements/drops",
619
+ body: params,
620
+ options
621
+ });
622
+ }
623
+ };
624
+
625
+ // src/api/extensions.ts
626
+ var ExtensionsApi = class extends ApiGroup {
627
+ configuration(params, options) {
628
+ return this.rest.data({
629
+ path: "extensions/configurations",
630
+ query: params,
631
+ options
632
+ });
633
+ }
634
+ setConfiguration(params, options) {
635
+ return this.rest.send({
636
+ method: "PUT",
637
+ path: "extensions/configurations",
638
+ body: params,
639
+ options
640
+ });
641
+ }
642
+ setRequiredConfiguration(params, options) {
643
+ const { broadcaster_id, ...body } = params;
644
+ return this.rest.send({
645
+ method: "PUT",
646
+ path: "extensions/required_configuration",
647
+ query: { broadcaster_id },
648
+ body,
649
+ options
650
+ });
651
+ }
652
+ sendPubSub(params, options) {
653
+ return this.rest.send({
654
+ method: "POST",
655
+ path: "extensions/pubsub",
656
+ body: params,
657
+ options
658
+ });
659
+ }
660
+ liveChannels(params, options) {
661
+ return this.rest.paginate({
662
+ path: "extensions/live",
663
+ query: params,
664
+ options
665
+ });
666
+ }
667
+ secrets(params, options) {
668
+ return this.rest.one({
669
+ path: "extensions/jwt/secrets",
670
+ query: params,
671
+ options
672
+ });
673
+ }
674
+ createSecret(params, options) {
675
+ return this.rest.one({
676
+ method: "POST",
677
+ path: "extensions/jwt/secrets",
678
+ query: params,
679
+ options
680
+ });
681
+ }
682
+ sendChatMessage(params, options) {
683
+ const { broadcaster_id, ...body } = params;
684
+ return this.rest.send({
685
+ method: "POST",
686
+ path: "extensions/chat",
687
+ query: { broadcaster_id },
688
+ body,
689
+ options
690
+ });
691
+ }
692
+ get(params, options) {
693
+ return this.rest.first({
694
+ path: "extensions",
695
+ query: params,
696
+ options
697
+ });
698
+ }
699
+ released(params, options) {
700
+ return this.rest.one({
701
+ path: "extensions/released",
702
+ query: params,
703
+ options
704
+ });
705
+ }
706
+ bitsProducts(params = {}, options) {
707
+ return this.rest.data({
708
+ path: "bits/extensions",
709
+ query: params,
710
+ auth: "app",
711
+ options
712
+ });
713
+ }
714
+ updateBitsProduct(params, options) {
715
+ return this.rest.one({
716
+ method: "PUT",
717
+ path: "bits/extensions",
718
+ body: params,
719
+ auth: "app",
720
+ options
721
+ });
722
+ }
723
+ };
724
+
725
+ // src/errors.ts
726
+ var TwitchError = class extends Error {
727
+ name = "TwitchError";
728
+ constructor(message, options) {
729
+ super(message, options);
730
+ }
731
+ };
732
+ var TwitchApiError = class extends TwitchError {
733
+ name = "TwitchApiError";
734
+ status;
735
+ method;
736
+ url;
737
+ error;
738
+ body;
739
+ headers;
740
+ constructor(message, info) {
741
+ super(message);
742
+ this.status = info.status;
743
+ this.method = info.method;
744
+ this.url = info.url;
745
+ this.error = info.error;
746
+ this.body = info.body;
747
+ this.headers = info.headers;
748
+ }
749
+ };
750
+ var TwitchRateLimitError = class extends TwitchApiError {
751
+ name = "TwitchRateLimitError";
752
+ resetAt;
753
+ constructor(message, info, resetAt) {
754
+ super(message, info);
755
+ this.resetAt = resetAt;
756
+ }
757
+ get retryAfter() {
758
+ return this.resetAt === void 0 ? 0 : Math.max(0, this.resetAt - Date.now());
759
+ }
760
+ };
761
+ var TwitchAuthError = class extends TwitchError {
762
+ name = "TwitchAuthError";
763
+ userId;
764
+ constructor(message, options) {
765
+ super(message, options);
766
+ this.userId = options?.userId;
767
+ }
768
+ };
769
+ var TwitchScopeError = class extends TwitchAuthError {
770
+ name = "TwitchScopeError";
771
+ required;
772
+ granted;
773
+ constructor(userId, required, granted, what) {
774
+ const list = required.map((s) => `"${s}"`).join(" or ");
775
+ super(`The token of user ${userId} is missing the ${list} scope required by ${what}.`, { userId });
776
+ this.required = required;
777
+ this.granted = granted;
778
+ }
779
+ };
780
+ var TwitchNetworkError = class extends TwitchError {
781
+ name = "TwitchNetworkError";
782
+ };
783
+ var TwitchTimeoutError = class extends TwitchError {
784
+ name = "TwitchTimeoutError";
785
+ };
786
+ var TwitchEventSubError = class extends TwitchError {
787
+ name = "TwitchEventSubError";
788
+ };
789
+
790
+ // src/eventsub/types/activity.ts
791
+ var activityVersions = {
792
+ "channel.guest_star_session.begin": "beta",
793
+ "channel.guest_star_session.end": "beta",
794
+ "channel.guest_star_guest.update": "beta",
795
+ "channel.guest_star_settings.update": "beta",
796
+ "channel.channel_points_automatic_reward_redemption.add": "2",
797
+ "channel.channel_points_automatic_reward_redemption.add@1": "1",
798
+ "channel.channel_points_custom_reward.add": "1",
799
+ "channel.channel_points_custom_reward.update": "1",
800
+ "channel.channel_points_custom_reward.remove": "1",
801
+ "channel.channel_points_custom_reward_redemption.add": "1",
802
+ "channel.channel_points_custom_reward_redemption.update": "1",
803
+ "channel.custom_power_up_redemption.add": "1",
804
+ "channel.poll.begin": "1",
805
+ "channel.poll.progress": "1",
806
+ "channel.poll.end": "1",
807
+ "channel.prediction.begin": "1",
808
+ "channel.prediction.progress": "1",
809
+ "channel.prediction.lock": "1",
810
+ "channel.prediction.end": "1",
811
+ "channel.charity_campaign.donate": "1",
812
+ "channel.charity_campaign.start": "1",
813
+ "channel.charity_campaign.progress": "1",
814
+ "channel.charity_campaign.stop": "1",
815
+ "conduit.shard.disabled": "1",
816
+ "drop.entitlement.grant": "1",
817
+ "extension.bits_transaction.create": "1",
818
+ "channel.goal.begin": "1",
819
+ "channel.goal.progress": "1",
820
+ "channel.goal.end": "1",
821
+ "channel.hype_train.begin": "2",
822
+ "channel.hype_train.progress": "2",
823
+ "channel.hype_train.end": "2",
824
+ "user.authorization.grant": "1",
825
+ "user.authorization.revoke": "1",
826
+ "user.update": "1",
827
+ "user.whisper.message": "1"
828
+ };
829
+
830
+ // src/eventsub/types/channel.ts
831
+ var channelVersions = {
832
+ "channel.update": "2",
833
+ "channel.follow": "2",
834
+ "channel.ad_break.begin": "1",
835
+ "channel.subscribe": "1",
836
+ "channel.subscription.end": "1",
837
+ "channel.subscription.gift": "1",
838
+ "channel.subscription.message": "1",
839
+ "channel.cheer": "1",
840
+ "channel.raid": "1",
841
+ "channel.ban": "1",
842
+ "channel.unban": "1",
843
+ "channel.unban_request.create": "1",
844
+ "channel.unban_request.resolve": "1",
845
+ "channel.moderate": "2",
846
+ "channel.moderate@1": "1",
847
+ "channel.moderator.add": "1",
848
+ "channel.moderator.remove": "1",
849
+ "channel.suspicious_user.message": "1",
850
+ "channel.suspicious_user.update": "1",
851
+ "channel.vip.add": "1",
852
+ "channel.vip.remove": "1",
853
+ "channel.warning.acknowledge": "1",
854
+ "channel.warning.send": "1",
855
+ "channel.shield_mode.begin": "1",
856
+ "channel.shield_mode.end": "1",
857
+ "channel.shoutout.create": "1",
858
+ "channel.shoutout.receive": "1",
859
+ "stream.online": "1",
860
+ "stream.offline": "1"
861
+ };
862
+
863
+ // src/eventsub/types/chat.ts
864
+ var chatVersions = {
865
+ "automod.message.hold": "2",
866
+ "automod.message.hold@1": "1",
867
+ "automod.message.update": "2",
868
+ "automod.message.update@1": "1",
869
+ "automod.settings.update": "1",
870
+ "automod.terms.update": "1",
871
+ "channel.bits.use": "1",
872
+ "channel.chat.clear": "1",
873
+ "channel.chat.clear_user_messages": "1",
874
+ "channel.chat.message": "1",
875
+ "channel.chat.message_delete": "1",
876
+ "channel.chat.notification": "1",
877
+ "channel.chat_settings.update": "1",
878
+ "channel.chat.user_message_hold": "1",
879
+ "channel.chat.user_message_update": "1",
880
+ "channel.shared_chat.begin": "1",
881
+ "channel.shared_chat.update": "1",
882
+ "channel.shared_chat.end": "1"
883
+ };
884
+
885
+ // src/eventsub/types/index.ts
886
+ var EVENTSUB_VERSIONS = {
887
+ ...activityVersions,
888
+ ...channelVersions,
889
+ ...chatVersions
890
+ };
891
+ function resolveEventSubType(key) {
892
+ const at = key.lastIndexOf("@");
893
+ if (at > 0) return { type: key.slice(0, at), version: key.slice(at + 1) };
894
+ const version = EVENTSUB_VERSIONS[key];
895
+ if (version === void 0) {
896
+ throw new TwitchEventSubError(`Unknown EventSub type "${key}". For a type newer than this library, write "${key}@<version>".`);
897
+ }
898
+ return { type: key, version };
899
+ }
900
+
901
+ // src/eventsub/listener.ts
902
+ function subscriptionKey(type, version, condition) {
903
+ const fields = Object.entries(condition).filter(([, value]) => value !== void 0 && value !== null && value !== "").map(([name, value]) => `${name}=${String(value)}`).sort();
904
+ return `${type}@${version}?${fields.join("&")}`;
905
+ }
906
+ var EventSubListener = class {
907
+ entries = /* @__PURE__ */ new Map();
908
+ #byId = /* @__PURE__ */ new Map();
909
+ #events = new Emitter();
910
+ on(type, condition, handler) {
911
+ const resolved = resolveEventSubType(type);
912
+ const cleaned = {};
913
+ for (const [name, value] of Object.entries(condition)) {
914
+ if (value !== void 0 && value !== null) cleaned[name] = String(value);
915
+ }
916
+ const key = subscriptionKey(resolved.type, resolved.version, cleaned);
917
+ const callback = handler;
918
+ let entry = this.entries.get(key);
919
+ if (entry) {
920
+ entry.handlers.add(callback);
921
+ } else {
922
+ let settle;
923
+ const ready = new Promise((resolve, reject) => settle = { resolve, reject });
924
+ ready.catch(() => {
925
+ });
926
+ entry = {
927
+ key,
928
+ type: resolved.type,
929
+ version: resolved.version,
930
+ condition: cleaned,
931
+ handlers: /* @__PURE__ */ new Set([callback]),
932
+ subscription: void 0,
933
+ ready,
934
+ settle,
935
+ settled: false
936
+ };
937
+ this.entries.set(key, entry);
938
+ const created = entry;
939
+ this.attach(created).catch((error) => this.reject(created, error));
940
+ }
941
+ const target = entry;
942
+ return {
943
+ type: target.type,
944
+ version: target.version,
945
+ condition: target.condition,
946
+ ready: target.ready,
947
+ stop: async () => {
948
+ target.handlers.delete(callback);
949
+ if (target.handlers.size > 0 || this.entries.get(target.key) !== target) return;
950
+ this.forget(target);
951
+ await this.detach(target);
952
+ }
953
+ };
954
+ }
955
+ onError(listener) {
956
+ return this.#events.on("error", listener);
957
+ }
958
+ onRevocation(listener) {
959
+ return this.#events.on("revocation", listener);
960
+ }
961
+ get size() {
962
+ return this.entries.size;
963
+ }
964
+ accept(entry, subscription) {
965
+ if (subscription) {
966
+ if (entry.subscription) this.#byId.delete(entry.subscription.id);
967
+ entry.subscription = subscription;
968
+ this.#byId.set(subscription.id, entry);
969
+ }
970
+ if (!entry.settled) {
971
+ entry.settled = true;
972
+ entry.settle.resolve();
973
+ }
974
+ }
975
+ reject(entry, cause) {
976
+ const reason = cause instanceof Error ? cause.message : String(cause);
977
+ const error = new TwitchEventSubError(`Could not subscribe to ${entry.type} (version ${entry.version}): ${reason}`, {
978
+ cause
979
+ });
980
+ this.expire(entry, error);
981
+ this.forget(entry);
982
+ }
983
+ expire(entry, error) {
984
+ if (!entry.settled) {
985
+ entry.settled = true;
986
+ entry.settle.reject(error);
987
+ }
988
+ this.fail(error);
989
+ }
990
+ active(entry) {
991
+ return this.entries.get(entry.key) === entry;
992
+ }
993
+ forget(entry) {
994
+ if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key);
995
+ if (entry.subscription) this.#byId.delete(entry.subscription.id);
996
+ if (!entry.settled) {
997
+ entry.settled = true;
998
+ entry.settle.reject(
999
+ new TwitchEventSubError(`The subscription to ${entry.type} was removed before Twitch confirmed it.`)
1000
+ );
1001
+ }
1002
+ }
1003
+ find(subscription) {
1004
+ return this.#byId.get(subscription.id) ?? this.entries.get(subscriptionKey(subscription.type, subscription.version, subscription.condition ?? {}));
1005
+ }
1006
+ dispatch(subscription, event, meta) {
1007
+ const entry = this.find(subscription);
1008
+ if (!entry) return Promise.resolve();
1009
+ if (!entry.subscription) this.accept(entry, subscription);
1010
+ const full = { ...meta, type: subscription.type, version: subscription.version, subscription };
1011
+ const running = [];
1012
+ for (const handler of [...entry.handlers]) {
1013
+ try {
1014
+ const result = handler(event, full);
1015
+ if (result && typeof result.then === "function") {
1016
+ running.push(result.then(void 0, (error) => this.fail(error)));
1017
+ }
1018
+ } catch (error) {
1019
+ this.fail(error);
1020
+ }
1021
+ }
1022
+ return Promise.all(running).then(() => void 0);
1023
+ }
1024
+ revoked(subscription) {
1025
+ const entry = this.find(subscription);
1026
+ if (entry) this.forget(entry);
1027
+ this.#events.emit("revocation", subscription);
1028
+ }
1029
+ fail(cause) {
1030
+ const error = cause instanceof Error ? cause : new TwitchEventSubError(String(cause));
1031
+ if (!this.#events.emit("error", error)) {
1032
+ console.error("[twitch.api] EventSub error (add an onError listener to handle it yourself):", error);
1033
+ }
1034
+ }
1035
+ };
1036
+
1037
+ // src/eventsub/webhook.ts
1038
+ var encoder = new TextEncoder();
1039
+ var VERIFICATION_TIMEOUT = 6e4;
1040
+ var MAX_REMEMBERED = 5e5;
1041
+ async function verifyEventSubSignature(input) {
1042
+ const { secret, messageId, timestamp, signature, body } = input;
1043
+ if (!messageId || !timestamp || !signature) return false;
1044
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
1045
+ "sign"
1046
+ ]);
1047
+ const digest = new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(messageId + timestamp + body)));
1048
+ let expected = "sha256=";
1049
+ for (const byte of digest) expected += byte.toString(16).padStart(2, "0");
1050
+ const given = signature.toLowerCase();
1051
+ let diff = expected.length ^ given.length;
1052
+ for (let i = 0; i < expected.length; i++) diff |= expected.charCodeAt(i) ^ (given.charCodeAt(i) || 0);
1053
+ return diff === 0;
1054
+ }
1055
+ var EventSubWebhook = class extends EventSubListener {
1056
+ #backend;
1057
+ #callback;
1058
+ #secret;
1059
+ #subscribe;
1060
+ #maxAge;
1061
+ #seen = /* @__PURE__ */ new Map();
1062
+ constructor(backend, options) {
1063
+ super();
1064
+ if (!options?.callback || !options.secret) {
1065
+ throw new TwitchEventSubError("A webhook listener needs a `callback` URL and a `secret`.");
1066
+ }
1067
+ if (options.secret.length < 10 || options.secret.length > 100) {
1068
+ throw new TwitchEventSubError("The webhook secret must be 10 to 100 characters long.");
1069
+ }
1070
+ this.#backend = backend;
1071
+ this.#callback = options.callback;
1072
+ this.#secret = options.secret;
1073
+ this.#subscribe = options.subscribe ?? true;
1074
+ this.#maxAge = (options.maxAge ?? 600) * 1e3;
1075
+ }
1076
+ async handle(request, context) {
1077
+ const result = await this.process({ header: (name) => request.headers.get(name), body: await request.text() }, context);
1078
+ return new Response(result.body ?? null, {
1079
+ status: result.status,
1080
+ headers: result.body === void 0 ? {} : { "Content-Type": "text/plain" }
1081
+ });
1082
+ }
1083
+ nodeHandler() {
1084
+ return async (req, res) => {
1085
+ let result;
1086
+ try {
1087
+ const body = await readNodeBody(req);
1088
+ result = await this.process({
1089
+ header: (name) => {
1090
+ const value = req.headers[name.toLowerCase()];
1091
+ return Array.isArray(value) ? value[0] : value;
1092
+ },
1093
+ body
1094
+ });
1095
+ } catch (error) {
1096
+ this.fail(error);
1097
+ result = { status: 500 };
1098
+ }
1099
+ if (result.body === void 0) res.writeHead(result.status);
1100
+ else res.writeHead(result.status, { "Content-Type": "text/plain" });
1101
+ res.end(result.body);
1102
+ };
1103
+ }
1104
+ async process(request, context) {
1105
+ const messageId = request.header("Twitch-Eventsub-Message-Id");
1106
+ const timestamp = request.header("Twitch-Eventsub-Message-Timestamp");
1107
+ const signature = request.header("Twitch-Eventsub-Message-Signature");
1108
+ const messageType = request.header("Twitch-Eventsub-Message-Type");
1109
+ if (!messageId || !timestamp || !signature || !messageType) return { status: 400 };
1110
+ const now = Date.now();
1111
+ const sentAt = Date.parse(timestamp);
1112
+ if (Number.isNaN(sentAt) || Math.abs(now - sentAt) > this.#maxAge) return { status: 403 };
1113
+ const valid = await verifyEventSubSignature({ secret: this.#secret, messageId, timestamp, signature, body: request.body });
1114
+ if (!valid) return { status: 403 };
1115
+ let payload;
1116
+ try {
1117
+ payload = JSON.parse(request.body);
1118
+ } catch {
1119
+ return { status: 400 };
1120
+ }
1121
+ if (typeof payload !== "object" || payload === null || typeof payload.subscription !== "object" || payload.subscription === null) {
1122
+ return { status: 400 };
1123
+ }
1124
+ if (messageType === "webhook_callback_verification") {
1125
+ const entry = this.find(payload.subscription);
1126
+ if (entry) this.accept(entry, payload.subscription);
1127
+ return { status: 200, body: payload.challenge ?? "" };
1128
+ }
1129
+ if (this.#remember(messageId, now)) return { status: 204 };
1130
+ if (messageType === "notification") {
1131
+ const work = this.dispatch(payload.subscription, payload.event ?? payload.events, { messageId, timestamp });
1132
+ context?.waitUntil?.(work);
1133
+ } else if (messageType === "revocation") {
1134
+ this.revoked(payload.subscription);
1135
+ }
1136
+ return { status: 204 };
1137
+ }
1138
+ async attach(entry) {
1139
+ if (!this.#subscribe) {
1140
+ this.accept(entry);
1141
+ return;
1142
+ }
1143
+ let subscription;
1144
+ try {
1145
+ subscription = await this.#backend.create({
1146
+ type: entry.type,
1147
+ version: entry.version,
1148
+ condition: entry.condition,
1149
+ transport: { method: "webhook", callback: this.#callback, secret: this.#secret }
1150
+ });
1151
+ } catch (error) {
1152
+ if (error instanceof TwitchApiError && error.status === 409) return this.accept(entry);
1153
+ throw error;
1154
+ }
1155
+ if (!this.active(entry)) {
1156
+ await this.#backend.remove(subscription.id, "app").catch(() => {
1157
+ });
1158
+ return;
1159
+ }
1160
+ if (subscription.status === "enabled") {
1161
+ this.accept(entry, subscription);
1162
+ return;
1163
+ }
1164
+ entry.subscription = subscription;
1165
+ unref(
1166
+ setTimeout(() => {
1167
+ if (entry.settled || !this.active(entry)) return;
1168
+ this.expire(
1169
+ entry,
1170
+ new TwitchEventSubError(
1171
+ `Twitch did not verify the callback for ${entry.type} within ${VERIFICATION_TIMEOUT / 1e3} seconds. Check that ${this.#callback} is public, uses HTTPS on port 443 and reaches this listener.`
1172
+ )
1173
+ );
1174
+ }, VERIFICATION_TIMEOUT)
1175
+ );
1176
+ }
1177
+ async detach(entry) {
1178
+ if (!this.#subscribe || !entry.subscription) return;
1179
+ await this.#backend.remove(entry.subscription.id, "app").catch((error) => {
1180
+ if (!(error instanceof TwitchApiError) || error.status !== 404) throw error;
1181
+ });
1182
+ }
1183
+ #remember(messageId, now) {
1184
+ for (const [id, seenAt] of this.#seen) {
1185
+ if (now - seenAt <= this.#maxAge && this.#seen.size <= MAX_REMEMBERED) break;
1186
+ this.#seen.delete(id);
1187
+ }
1188
+ if (this.#seen.has(messageId)) return true;
1189
+ this.#seen.set(messageId, now);
1190
+ return false;
1191
+ }
1192
+ };
1193
+ async function readNodeBody(req) {
1194
+ const parsed = req.body;
1195
+ if (typeof parsed === "string") return parsed;
1196
+ if (parsed instanceof Uint8Array) return new TextDecoder().decode(parsed);
1197
+ if (parsed !== void 0 && parsed !== null && typeof parsed === "object" && Object.keys(parsed).length > 0) {
1198
+ throw new TwitchEventSubError(
1199
+ "The request body was already parsed by a middleware, so its signature cannot be verified. Mount the EventSub handler before JSON body parsers, or use a raw body parser on its route."
1200
+ );
1201
+ }
1202
+ const chunks = [];
1203
+ let length = 0;
1204
+ for await (const chunk2 of req) {
1205
+ const bytes = typeof chunk2 === "string" ? encoder.encode(chunk2) : chunk2;
1206
+ chunks.push(bytes);
1207
+ length += bytes.length;
1208
+ }
1209
+ const all = new Uint8Array(length);
1210
+ let offset = 0;
1211
+ for (const bytes of chunks) {
1212
+ all.set(bytes, offset);
1213
+ offset += bytes.length;
1214
+ }
1215
+ return new TextDecoder().decode(all);
1216
+ }
1217
+
1218
+ // src/eventsub/websocket.ts
1219
+ var DEFAULT_URL = "wss://eventsub.wss.twitch.tv/ws";
1220
+ var KEEPALIVE_GRACE = 5e3;
1221
+ var SUBSCRIBE_BATCH = 20;
1222
+ var SUBSCRIBE_ATTEMPTS = 6;
1223
+ var Connection = class {
1224
+ entries = /* @__PURE__ */ new Set();
1225
+ sessionId;
1226
+ ready = false;
1227
+ #socket;
1228
+ #pending;
1229
+ #watchdog;
1230
+ #retry;
1231
+ #keepaliveMs = 1e4;
1232
+ #attempts = 0;
1233
+ #closed = false;
1234
+ #opened = false;
1235
+ #owner;
1236
+ constructor(owner) {
1237
+ this.#owner = owner;
1238
+ }
1239
+ ensureOpen() {
1240
+ if (this.#opened || this.#closed) return;
1241
+ this.#opened = true;
1242
+ this.#open(this.#owner.url, false);
1243
+ }
1244
+ close() {
1245
+ this.#closed = true;
1246
+ this.sessionId = void 0;
1247
+ this.ready = false;
1248
+ clearTimeout(this.#watchdog);
1249
+ clearTimeout(this.#retry);
1250
+ for (const socket of [this.#socket, this.#pending]) if (socket) this.#release(socket);
1251
+ this.#socket = this.#pending = void 0;
1252
+ }
1253
+ #open(url, handover) {
1254
+ let socket;
1255
+ try {
1256
+ socket = new this.#owner.WebSocket(url);
1257
+ } catch (error) {
1258
+ this.#owner.report(new TwitchEventSubError(`Could not open the EventSub WebSocket: ${error.message}`, { cause: error }));
1259
+ this.#scheduleReconnect();
1260
+ return;
1261
+ }
1262
+ if (handover) this.#pending = socket;
1263
+ else this.#socket = socket;
1264
+ socket.onmessage = (event) => this.#message(socket, event.data);
1265
+ socket.onclose = (event) => this.#lost(socket, event?.code, event?.reason);
1266
+ socket.onerror = () => {
1267
+ };
1268
+ if (!handover) this.#arm(15e3);
1269
+ }
1270
+ #release(socket) {
1271
+ socket.onmessage = socket.onclose = socket.onerror = socket.onopen = null;
1272
+ try {
1273
+ socket.close();
1274
+ } catch {
1275
+ }
1276
+ }
1277
+ #arm(ms = this.#keepaliveMs + KEEPALIVE_GRACE) {
1278
+ clearTimeout(this.#watchdog);
1279
+ this.#watchdog = unref(
1280
+ setTimeout(() => {
1281
+ const socket = this.#socket;
1282
+ if (socket) this.#lost(socket, void 0, "keepalive timeout");
1283
+ }, ms)
1284
+ );
1285
+ }
1286
+ #message(socket, data) {
1287
+ let frame;
1288
+ try {
1289
+ frame = JSON.parse(typeof data === "string" ? data : String(data));
1290
+ } catch {
1291
+ return;
1292
+ }
1293
+ const { metadata, payload } = frame;
1294
+ switch (metadata?.message_type) {
1295
+ case "session_welcome": {
1296
+ const session = payload.session;
1297
+ this.#keepaliveMs = (session.keepalive_timeout_seconds ?? 10) * 1e3;
1298
+ this.#attempts = 0;
1299
+ if (socket === this.#pending) {
1300
+ const old = this.#socket;
1301
+ this.#socket = socket;
1302
+ this.#pending = void 0;
1303
+ this.sessionId = session.id;
1304
+ if (old) this.#release(old);
1305
+ this.#arm();
1306
+ this.#owner.handedOver(this, session.id);
1307
+ return;
1308
+ }
1309
+ this.sessionId = session.id;
1310
+ this.#arm();
1311
+ this.#owner.established(this, session.id);
1312
+ return;
1313
+ }
1314
+ case "session_keepalive":
1315
+ if (socket === this.#socket) this.#arm();
1316
+ return;
1317
+ case "notification":
1318
+ if (socket === this.#socket) this.#arm();
1319
+ if (payload.subscription) this.#owner.notify(metadata, payload.subscription, payload.event ?? payload.events);
1320
+ return;
1321
+ case "session_reconnect":
1322
+ if (payload.session?.reconnect_url && !this.#pending) this.#open(payload.session.reconnect_url, true);
1323
+ return;
1324
+ case "revocation":
1325
+ if (socket === this.#socket) this.#arm();
1326
+ if (payload.subscription) this.#owner.revoke(this, payload.subscription);
1327
+ return;
1328
+ }
1329
+ }
1330
+ #lost(socket, code, reason) {
1331
+ if (this.#closed) return;
1332
+ if (socket === this.#pending) {
1333
+ this.#pending = void 0;
1334
+ this.#release(socket);
1335
+ return;
1336
+ }
1337
+ if (socket !== this.#socket) return;
1338
+ clearTimeout(this.#watchdog);
1339
+ this.#release(socket);
1340
+ if (this.#pending) this.#release(this.#pending);
1341
+ this.#socket = this.#pending = void 0;
1342
+ this.sessionId = void 0;
1343
+ this.ready = false;
1344
+ const willReconnect = this.entries.size > 0;
1345
+ this.#owner.dropped(this, { code, reason, willReconnect });
1346
+ if (willReconnect) this.#scheduleReconnect();
1347
+ else this.#opened = false;
1348
+ }
1349
+ #scheduleReconnect() {
1350
+ if (this.#closed) return;
1351
+ const delay = backoff(this.#attempts++, 500, 3e4);
1352
+ this.#retry = setTimeout(() => {
1353
+ if (!this.#closed) this.#open(this.#owner.url, false);
1354
+ }, delay);
1355
+ }
1356
+ };
1357
+ var EventSubWebSocket = class extends EventSubListener {
1358
+ url;
1359
+ WebSocket;
1360
+ #backend;
1361
+ #strategy;
1362
+ #connections = [];
1363
+ #where = /* @__PURE__ */ new Map();
1364
+ #seen = new BoundedMap(1e3);
1365
+ #lifecycle = new Emitter();
1366
+ constructor(backend, strategy, options = {}) {
1367
+ super();
1368
+ const WebSocketClass = options.webSocket ?? globalThis.WebSocket;
1369
+ if (!WebSocketClass) {
1370
+ throw new TwitchEventSubError(
1371
+ "This runtime has no global WebSocket. Pass one with the `webSocket` option (for example the `ws` package)."
1372
+ );
1373
+ }
1374
+ this.WebSocket = WebSocketClass;
1375
+ const url = new URL(options.url ?? DEFAULT_URL);
1376
+ if (options.keepalive !== void 0) url.searchParams.set("keepalive_timeout_seconds", String(options.keepalive));
1377
+ this.url = url.toString();
1378
+ this.#backend = backend;
1379
+ this.#strategy = strategy;
1380
+ }
1381
+ get sessionIds() {
1382
+ return this.#connections.flatMap((connection) => connection.sessionId ? [connection.sessionId] : []);
1383
+ }
1384
+ get connected() {
1385
+ return this.sessionIds.length > 0;
1386
+ }
1387
+ onConnect(listener) {
1388
+ return this.#lifecycle.on("connect", (sessionId) => this.#guard(() => listener(sessionId)));
1389
+ }
1390
+ onDisconnect(listener) {
1391
+ return this.#lifecycle.on("disconnect", (info) => this.#guard(() => listener(info)));
1392
+ }
1393
+ #guard(run) {
1394
+ try {
1395
+ const result = run();
1396
+ if (result && typeof result.then === "function") {
1397
+ result.then(void 0, (error) => this.fail(error));
1398
+ }
1399
+ } catch (error) {
1400
+ this.fail(error);
1401
+ }
1402
+ }
1403
+ close() {
1404
+ for (const connection of this.#connections) connection.close();
1405
+ this.#connections.length = 0;
1406
+ this.#where.clear();
1407
+ for (const entry of [...this.entries.values()]) this.forget(entry);
1408
+ }
1409
+ async attach(entry) {
1410
+ let connection = this.#connections.find((candidate) => candidate.entries.size < this.#strategy.capacity);
1411
+ if (!connection) {
1412
+ if (this.#connections.length >= this.#strategy.maxSockets) {
1413
+ throw new TwitchEventSubError(
1414
+ `Subscription limit reached: ${this.#strategy.maxSockets} connections of ${this.#strategy.capacity} subscriptions.`
1415
+ );
1416
+ }
1417
+ connection = new Connection(this);
1418
+ this.#connections.push(connection);
1419
+ }
1420
+ connection.entries.add(entry);
1421
+ this.#where.set(entry, connection);
1422
+ if (connection.ready && connection.sessionId) await this.#subscribe(entry, connection, connection.sessionId);
1423
+ else connection.ensureOpen();
1424
+ }
1425
+ async detach(entry) {
1426
+ const connection = this.#where.get(entry);
1427
+ this.#where.delete(entry);
1428
+ if (!connection) return;
1429
+ connection.entries.delete(entry);
1430
+ const subscription = entry.subscription;
1431
+ const alive = connection.sessionId !== void 0;
1432
+ if (connection.entries.size === 0) {
1433
+ connection.close();
1434
+ this.#connections.splice(this.#connections.indexOf(connection), 1);
1435
+ }
1436
+ if (subscription && (alive || this.#strategy.persistent)) {
1437
+ await this.#backend.remove(subscription.id, this.#strategy.auth, this.#strategy.options).catch((error) => {
1438
+ if (!(error instanceof TwitchApiError) || error.status !== 404) throw error;
1439
+ });
1440
+ }
1441
+ }
1442
+ async #subscribe(entry, connection, sessionId) {
1443
+ const current = () => this.#where.get(entry) === connection && connection.sessionId === sessionId;
1444
+ for (let attempt = 0; current(); attempt++) {
1445
+ try {
1446
+ const subscription = await this.#backend.create(
1447
+ {
1448
+ type: entry.type,
1449
+ version: entry.version,
1450
+ condition: entry.condition,
1451
+ transport: this.#strategy.transport(sessionId)
1452
+ },
1453
+ this.#strategy.options
1454
+ );
1455
+ if (this.#where.get(entry) !== connection) {
1456
+ await this.#backend.remove(subscription.id, this.#strategy.auth, this.#strategy.options).catch(() => {
1457
+ });
1458
+ return;
1459
+ }
1460
+ this.accept(entry, subscription);
1461
+ return;
1462
+ } catch (error) {
1463
+ if (!current()) return;
1464
+ if (error instanceof TwitchApiError && error.status === 409 && this.#strategy.persistent) {
1465
+ this.accept(entry);
1466
+ return;
1467
+ }
1468
+ if (error instanceof TwitchApiError && error.status === 400 && /session/i.test(error.message)) return;
1469
+ if (!isTemporary(error) || attempt + 1 >= SUBSCRIBE_ATTEMPTS) {
1470
+ connection.entries.delete(entry);
1471
+ this.#where.delete(entry);
1472
+ if (connection.ready) this.#retire(connection);
1473
+ throw error;
1474
+ }
1475
+ await sleep(backoff(attempt, 500, 3e4));
1476
+ }
1477
+ }
1478
+ }
1479
+ established(connection, sessionId) {
1480
+ void (async () => {
1481
+ for (let attempt = 0; ; attempt++) {
1482
+ try {
1483
+ await this.#strategy.onSession?.(sessionId);
1484
+ break;
1485
+ } catch (error) {
1486
+ if (connection.sessionId !== sessionId) return;
1487
+ const problem = new TwitchEventSubError(`Could not bind the WebSocket session: ${error.message}`, {
1488
+ cause: error
1489
+ });
1490
+ if (!isTemporary(error)) {
1491
+ const waiting = [...connection.entries].filter((entry) => !entry.settled);
1492
+ if (waiting.length === 0) this.fail(problem);
1493
+ for (const entry of waiting) {
1494
+ connection.entries.delete(entry);
1495
+ this.#where.delete(entry);
1496
+ this.reject(entry, problem);
1497
+ }
1498
+ this.#retire(connection);
1499
+ return;
1500
+ }
1501
+ if (attempt === 0) this.fail(problem);
1502
+ await sleep(backoff(attempt, 500, 3e4));
1503
+ }
1504
+ }
1505
+ if (connection.sessionId !== sessionId) return;
1506
+ const pending = [...connection.entries].filter((entry) => !(this.#strategy.persistent && entry.settled));
1507
+ connection.ready = true;
1508
+ this.#lifecycle.emit("connect", sessionId);
1509
+ for (let start = 0; start < pending.length; start += SUBSCRIBE_BATCH) {
1510
+ if (connection.sessionId !== sessionId) return;
1511
+ await Promise.all(
1512
+ pending.slice(start, start + SUBSCRIBE_BATCH).map(
1513
+ (entry) => this.#subscribe(entry, connection, sessionId).catch((error) => this.reject(entry, error))
1514
+ )
1515
+ );
1516
+ }
1517
+ this.#retire(connection);
1518
+ })();
1519
+ }
1520
+ #retire(connection) {
1521
+ if (connection.entries.size > 0) return;
1522
+ connection.close();
1523
+ const index = this.#connections.indexOf(connection);
1524
+ if (index >= 0) this.#connections.splice(index, 1);
1525
+ }
1526
+ handedOver(_connection, sessionId) {
1527
+ void (async () => {
1528
+ try {
1529
+ await this.#strategy.onSession?.(sessionId);
1530
+ } catch (error) {
1531
+ this.fail(error);
1532
+ }
1533
+ this.#lifecycle.emit("connect", sessionId);
1534
+ })();
1535
+ }
1536
+ dropped(connection, info) {
1537
+ this.#lifecycle.emit("disconnect", info);
1538
+ if (!info.willReconnect) {
1539
+ const index = this.#connections.indexOf(connection);
1540
+ if (index >= 0 && connection.entries.size === 0) this.#connections.splice(index, 1);
1541
+ }
1542
+ }
1543
+ notify(metadata, subscription, event) {
1544
+ if (this.#seen.has(metadata.message_id)) return;
1545
+ this.#seen.set(metadata.message_id, true);
1546
+ void this.dispatch(subscription, event, { messageId: metadata.message_id, timestamp: metadata.message_timestamp });
1547
+ }
1548
+ revoke(connection, subscription) {
1549
+ const entry = this.find(subscription);
1550
+ if (entry) {
1551
+ connection.entries.delete(entry);
1552
+ this.#where.delete(entry);
1553
+ }
1554
+ this.revoked(subscription);
1555
+ }
1556
+ report(error) {
1557
+ this.fail(error);
1558
+ }
1559
+ };
1560
+ function isTemporary(error) {
1561
+ if (error instanceof TwitchApiError) return error.status >= 500 || error.status === 429;
1562
+ return error instanceof TwitchNetworkError || error instanceof TwitchTimeoutError;
1563
+ }
1564
+
1565
+ // src/api/eventsub.ts
1566
+ function subscriptionBody(body) {
1567
+ return body.type === "drop.entitlement.grant" ? { ...body, is_batching_enabled: true } : body;
1568
+ }
1569
+ var EventSubApi = class extends ApiGroup {
1570
+ subscribe(params, options) {
1571
+ const { type, version } = resolveEventSubType(params.type);
1572
+ return this.rest.one({
1573
+ method: "POST",
1574
+ path: "eventsub/subscriptions",
1575
+ body: subscriptionBody({ type, version, condition: params.condition, transport: params.transport }),
1576
+ auth: params.transport.method === "websocket" ? "user" : "app",
1577
+ options
1578
+ });
1579
+ }
1580
+ unsubscribe(id, options) {
1581
+ return this.rest.send({ method: "DELETE", path: "eventsub/subscriptions", query: { id }, options });
1582
+ }
1583
+ subscriptions(params = {}, options) {
1584
+ return this.rest.paginate(
1585
+ { path: "eventsub/subscriptions", query: params, options },
1586
+ { sizeParam: null }
1587
+ );
1588
+ }
1589
+ websocket(options = {}) {
1590
+ const requestOptions = options.user ? { as: options.user } : {};
1591
+ return this.#track(new EventSubWebSocket(
1592
+ this.#backend(),
1593
+ {
1594
+ capacity: 300,
1595
+ maxSockets: 3,
1596
+ persistent: false,
1597
+ auth: "user",
1598
+ options: requestOptions,
1599
+ transport: (sessionId) => ({ method: "websocket", session_id: sessionId })
1600
+ },
1601
+ options
1602
+ ));
1603
+ }
1604
+ webhook(options) {
1605
+ return new EventSubWebhook(this.#backend(), options);
1606
+ }
1607
+ conduit(options = {}) {
1608
+ const shardId = options.shardId ?? "0";
1609
+ let conduitId;
1610
+ const resolveConduit = () => conduitId ??= (async () => {
1611
+ if (options.conduitId) return options.conduitId;
1612
+ const existing = await this.rest.data({ path: "eventsub/conduits", auth: "app" });
1613
+ if (existing[0]) return existing[0].id;
1614
+ const created = await this.rest.one({
1615
+ method: "POST",
1616
+ path: "eventsub/conduits",
1617
+ body: { shard_count: Number(shardId) + 1 },
1618
+ auth: "app"
1619
+ });
1620
+ return created.id;
1621
+ })().catch((error) => {
1622
+ conduitId = void 0;
1623
+ throw error;
1624
+ });
1625
+ let boundTo;
1626
+ return this.#track(new EventSubWebSocket(
1627
+ this.#backend(),
1628
+ {
1629
+ capacity: Infinity,
1630
+ maxSockets: 1,
1631
+ persistent: true,
1632
+ auth: "app",
1633
+ options: {},
1634
+ transport: () => {
1635
+ if (!boundTo) throw new TwitchEventSubError("The conduit is not bound to a session yet.");
1636
+ return { method: "conduit", conduit_id: boundTo };
1637
+ },
1638
+ onSession: async (sessionId) => {
1639
+ const id = await resolveConduit();
1640
+ const body = await this.rest.request({
1641
+ method: "PATCH",
1642
+ path: "eventsub/conduits/shards",
1643
+ body: {
1644
+ conduit_id: id,
1645
+ shards: [{ id: shardId, transport: { method: "websocket", session_id: sessionId } }]
1646
+ },
1647
+ auth: "app"
1648
+ });
1649
+ const failure = body?.errors?.[0];
1650
+ if (failure) throw new TwitchEventSubError(`Shard ${shardId} of conduit ${id}: ${failure.message}`);
1651
+ boundTo = id;
1652
+ }
1653
+ },
1654
+ options
1655
+ ));
1656
+ }
1657
+ #track(listener) {
1658
+ this.rest.onClose(() => listener.close());
1659
+ return listener;
1660
+ }
1661
+ #backend() {
1662
+ return {
1663
+ create: (params, options) => this.rest.one({
1664
+ method: "POST",
1665
+ path: "eventsub/subscriptions",
1666
+ body: subscriptionBody(params),
1667
+ auth: params.transport.method === "websocket" ? "user" : "app",
1668
+ options
1669
+ }),
1670
+ remove: (id, auth, options) => this.rest.send({ method: "DELETE", path: "eventsub/subscriptions", query: { id }, auth, options })
1671
+ };
1672
+ }
1673
+ };
1674
+
1675
+ // src/api/games.ts
1676
+ var GamesApi = class extends ApiGroup {
1677
+ top(params = {}, options) {
1678
+ return this.rest.paginate({ path: "games/top", query: params, options });
1679
+ }
1680
+ get(params, options) {
1681
+ return this.rest.chunked({ path: "games", query: params, options }, ["id", "name", "igdb_id"]);
1682
+ }
1683
+ };
1684
+
1685
+ // src/api/goals.ts
1686
+ var GoalsApi = class extends ApiGroup {
1687
+ get(params = {}, options) {
1688
+ return this.rest.data({
1689
+ path: "goals",
1690
+ query: params,
1691
+ auth: "user",
1692
+ scopes: ["channel:read:goals"],
1693
+ context: "broadcaster_id",
1694
+ options
1695
+ });
1696
+ }
1697
+ };
1698
+
1699
+ // src/api/guest-star.ts
1700
+ var GuestStarApi = class extends ApiGroup {
1701
+ settings(params, options) {
1702
+ return this.rest.one({
1703
+ path: "guest_star/channel_settings",
1704
+ query: params,
1705
+ auth: "user",
1706
+ scopes: [
1707
+ "channel:read:guest_star",
1708
+ "channel:manage:guest_star",
1709
+ "moderator:read:guest_star",
1710
+ "moderator:manage:guest_star"
1711
+ ],
1712
+ context: "moderator_id",
1713
+ options
1714
+ });
1715
+ }
1716
+ updateSettings(params = {}, options) {
1717
+ const { broadcaster_id, ...body } = params;
1718
+ return this.rest.send({
1719
+ method: "PUT",
1720
+ path: "guest_star/channel_settings",
1721
+ query: { broadcaster_id },
1722
+ body,
1723
+ auth: "user",
1724
+ scopes: ["channel:manage:guest_star"],
1725
+ context: "broadcaster_id",
1726
+ options
1727
+ });
1728
+ }
1729
+ session(params, options) {
1730
+ return this.rest.first({
1731
+ path: "guest_star/session",
1732
+ query: params,
1733
+ auth: "user",
1734
+ scopes: [
1735
+ "channel:read:guest_star",
1736
+ "channel:manage:guest_star",
1737
+ "moderator:read:guest_star",
1738
+ "moderator:manage:guest_star"
1739
+ ],
1740
+ context: "moderator_id",
1741
+ options
1742
+ });
1743
+ }
1744
+ createSession(params = {}, options) {
1745
+ return this.rest.one({
1746
+ method: "POST",
1747
+ path: "guest_star/session",
1748
+ query: params,
1749
+ auth: "user",
1750
+ scopes: ["channel:manage:guest_star"],
1751
+ context: "broadcaster_id",
1752
+ options
1753
+ });
1754
+ }
1755
+ endSession(params, options) {
1756
+ return this.rest.first({
1757
+ method: "DELETE",
1758
+ path: "guest_star/session",
1759
+ query: params,
1760
+ auth: "user",
1761
+ scopes: ["channel:manage:guest_star"],
1762
+ context: "broadcaster_id",
1763
+ options
1764
+ });
1765
+ }
1766
+ invites(params, options) {
1767
+ return this.rest.data({
1768
+ path: "guest_star/invites",
1769
+ query: params,
1770
+ auth: "user",
1771
+ scopes: [
1772
+ "channel:read:guest_star",
1773
+ "channel:manage:guest_star",
1774
+ "moderator:read:guest_star",
1775
+ "moderator:manage:guest_star"
1776
+ ],
1777
+ context: "moderator_id",
1778
+ options
1779
+ });
1780
+ }
1781
+ invite(params, options) {
1782
+ return this.rest.send({
1783
+ method: "POST",
1784
+ path: "guest_star/invites",
1785
+ query: params,
1786
+ auth: "user",
1787
+ scopes: ["channel:manage:guest_star", "moderator:manage:guest_star"],
1788
+ context: "moderator_id",
1789
+ options
1790
+ });
1791
+ }
1792
+ deleteInvite(params, options) {
1793
+ return this.rest.send({
1794
+ method: "DELETE",
1795
+ path: "guest_star/invites",
1796
+ query: params,
1797
+ auth: "user",
1798
+ scopes: ["channel:manage:guest_star", "moderator:manage:guest_star"],
1799
+ context: "moderator_id",
1800
+ options
1801
+ });
1802
+ }
1803
+ assignSlot(params, options) {
1804
+ return this.rest.send({
1805
+ method: "POST",
1806
+ path: "guest_star/slot",
1807
+ query: params,
1808
+ auth: "user",
1809
+ scopes: ["channel:manage:guest_star", "moderator:manage:guest_star"],
1810
+ context: "moderator_id",
1811
+ options
1812
+ });
1813
+ }
1814
+ updateSlot(params, options) {
1815
+ return this.rest.send({
1816
+ method: "PATCH",
1817
+ path: "guest_star/slot",
1818
+ query: params,
1819
+ auth: "user",
1820
+ scopes: ["channel:manage:guest_star", "moderator:manage:guest_star"],
1821
+ context: "moderator_id",
1822
+ options
1823
+ });
1824
+ }
1825
+ deleteSlot(params, options) {
1826
+ return this.rest.send({
1827
+ method: "DELETE",
1828
+ path: "guest_star/slot",
1829
+ query: params,
1830
+ auth: "user",
1831
+ scopes: ["channel:manage:guest_star", "moderator:manage:guest_star"],
1832
+ context: "moderator_id",
1833
+ options
1834
+ });
1835
+ }
1836
+ updateSlotSettings(params, options) {
1837
+ return this.rest.send({
1838
+ method: "PATCH",
1839
+ path: "guest_star/slot_settings",
1840
+ query: params,
1841
+ auth: "user",
1842
+ scopes: ["channel:manage:guest_star", "moderator:manage:guest_star"],
1843
+ context: "moderator_id",
1844
+ options
1845
+ });
1846
+ }
1847
+ };
1848
+
1849
+ // src/api/hype-train.ts
1850
+ var HypeTrainApi = class extends ApiGroup {
1851
+ status(params = {}, options) {
1852
+ return this.rest.one({
1853
+ path: "hypetrain/status",
1854
+ query: params,
1855
+ auth: "user",
1856
+ scopes: ["channel:read:hype_train"],
1857
+ context: "broadcaster_id",
1858
+ options
1859
+ });
1860
+ }
1861
+ };
1862
+
1863
+ // src/api/moderation.ts
1864
+ var ModerationApi = class extends ApiGroup {
1865
+ checkAutoMod(params, options) {
1866
+ const { data, ...query } = params;
1867
+ return this.rest.data({
1868
+ method: "POST",
1869
+ path: "moderation/enforcements/status",
1870
+ query,
1871
+ body: { data },
1872
+ scopes: ["moderation:read"],
1873
+ context: "broadcaster_id",
1874
+ options
1875
+ });
1876
+ }
1877
+ manageAutoModMessage(params, options) {
1878
+ return this.rest.send({
1879
+ method: "POST",
1880
+ path: "moderation/automod/message",
1881
+ body: params,
1882
+ scopes: ["moderator:manage:automod"],
1883
+ context: { body: "user_id" },
1884
+ options
1885
+ });
1886
+ }
1887
+ autoModSettings(params, options) {
1888
+ return this.rest.one({
1889
+ path: "moderation/automod/settings",
1890
+ query: params,
1891
+ scopes: ["moderator:read:automod_settings", "moderator:manage:automod_settings"],
1892
+ context: "moderator_id",
1893
+ options
1894
+ });
1895
+ }
1896
+ updateAutoModSettings(params, options) {
1897
+ const { broadcaster_id, moderator_id, ...body } = params;
1898
+ return this.rest.one({
1899
+ method: "PUT",
1900
+ path: "moderation/automod/settings",
1901
+ query: { broadcaster_id, moderator_id },
1902
+ body,
1903
+ scopes: ["moderator:manage:automod_settings"],
1904
+ context: "moderator_id",
1905
+ options
1906
+ });
1907
+ }
1908
+ bans(params = {}, options) {
1909
+ return this.rest.paginate({
1910
+ path: "moderation/banned",
1911
+ query: params,
1912
+ scopes: ["moderation:read", "moderator:manage:banned_users"],
1913
+ context: "broadcaster_id",
1914
+ options
1915
+ });
1916
+ }
1917
+ ban(params, options) {
1918
+ const { broadcaster_id, moderator_id, ...data } = params;
1919
+ return this.rest.one({
1920
+ method: "POST",
1921
+ path: "moderation/bans",
1922
+ query: { broadcaster_id, moderator_id },
1923
+ body: { data },
1924
+ scopes: ["moderator:manage:banned_users"],
1925
+ context: "moderator_id",
1926
+ options
1927
+ });
1928
+ }
1929
+ unban(params, options) {
1930
+ return this.rest.send({
1931
+ method: "DELETE",
1932
+ path: "moderation/bans",
1933
+ query: params,
1934
+ scopes: ["moderator:manage:banned_users"],
1935
+ context: "moderator_id",
1936
+ options
1937
+ });
1938
+ }
1939
+ unbanRequests(params, options) {
1940
+ return this.rest.paginate({
1941
+ path: "moderation/unban_requests",
1942
+ query: params,
1943
+ auth: "user",
1944
+ scopes: ["moderator:read:unban_requests", "moderator:manage:unban_requests"],
1945
+ context: "moderator_id",
1946
+ options
1947
+ });
1948
+ }
1949
+ resolveUnbanRequest(params, options) {
1950
+ return this.rest.one({
1951
+ method: "PATCH",
1952
+ path: "moderation/unban_requests",
1953
+ query: params,
1954
+ auth: "user",
1955
+ scopes: ["moderator:manage:unban_requests"],
1956
+ context: "moderator_id",
1957
+ options
1958
+ });
1959
+ }
1960
+ blockedTerms(params, options) {
1961
+ return this.rest.paginate({
1962
+ path: "moderation/blocked_terms",
1963
+ query: params,
1964
+ scopes: ["moderator:read:blocked_terms", "moderator:manage:blocked_terms"],
1965
+ context: "moderator_id",
1966
+ options
1967
+ });
1968
+ }
1969
+ addBlockedTerm(params, options) {
1970
+ const { text, ...query } = params;
1971
+ return this.rest.one({
1972
+ method: "POST",
1973
+ path: "moderation/blocked_terms",
1974
+ query,
1975
+ body: { text },
1976
+ scopes: ["moderator:manage:blocked_terms"],
1977
+ context: "moderator_id",
1978
+ options
1979
+ });
1980
+ }
1981
+ removeBlockedTerm(params, options) {
1982
+ return this.rest.send({
1983
+ method: "DELETE",
1984
+ path: "moderation/blocked_terms",
1985
+ query: params,
1986
+ scopes: ["moderator:manage:blocked_terms"],
1987
+ context: "moderator_id",
1988
+ options
1989
+ });
1990
+ }
1991
+ deleteMessages(params, options) {
1992
+ return this.rest.send({
1993
+ method: "DELETE",
1994
+ path: "moderation/chat",
1995
+ query: params,
1996
+ scopes: ["moderator:manage:chat_messages"],
1997
+ context: "moderator_id",
1998
+ options
1999
+ });
2000
+ }
2001
+ moderatedChannels(params = {}, options) {
2002
+ return this.rest.paginate({
2003
+ path: "moderation/channels",
2004
+ query: params,
2005
+ scopes: ["user:read:moderated_channels"],
2006
+ context: "user_id",
2007
+ options
2008
+ });
2009
+ }
2010
+ moderators(params = {}, options) {
2011
+ return this.rest.paginate({
2012
+ path: "moderation/moderators",
2013
+ query: params,
2014
+ auth: "user",
2015
+ scopes: ["moderation:read", "channel:manage:moderators"],
2016
+ context: "broadcaster_id",
2017
+ options
2018
+ });
2019
+ }
2020
+ addModerator(params, options) {
2021
+ return this.rest.send({
2022
+ method: "POST",
2023
+ path: "moderation/moderators",
2024
+ query: params,
2025
+ auth: "user",
2026
+ scopes: ["channel:manage:moderators"],
2027
+ context: "broadcaster_id",
2028
+ options
2029
+ });
2030
+ }
2031
+ removeModerator(params, options) {
2032
+ return this.rest.send({
2033
+ method: "DELETE",
2034
+ path: "moderation/moderators",
2035
+ query: params,
2036
+ auth: "user",
2037
+ scopes: ["channel:manage:moderators"],
2038
+ context: "broadcaster_id",
2039
+ options
2040
+ });
2041
+ }
2042
+ vips(params = {}, options) {
2043
+ return this.rest.paginate({
2044
+ path: "channels/vips",
2045
+ query: params,
2046
+ auth: "user",
2047
+ scopes: ["channel:read:vips", "channel:manage:vips"],
2048
+ context: "broadcaster_id",
2049
+ options
2050
+ });
2051
+ }
2052
+ addVip(params, options) {
2053
+ return this.rest.send({
2054
+ method: "POST",
2055
+ path: "channels/vips",
2056
+ query: params,
2057
+ auth: "user",
2058
+ scopes: ["channel:manage:vips"],
2059
+ context: "broadcaster_id",
2060
+ options
2061
+ });
2062
+ }
2063
+ removeVip(params, options) {
2064
+ return this.rest.send({
2065
+ method: "DELETE",
2066
+ path: "channels/vips",
2067
+ query: params,
2068
+ auth: "user",
2069
+ scopes: ["channel:manage:vips"],
2070
+ context: "broadcaster_id",
2071
+ options
2072
+ });
2073
+ }
2074
+ updateShieldMode(params, options) {
2075
+ const { is_active, ...query } = params;
2076
+ return this.rest.one({
2077
+ method: "PUT",
2078
+ path: "moderation/shield_mode",
2079
+ query,
2080
+ body: { is_active },
2081
+ scopes: ["moderator:manage:shield_mode"],
2082
+ context: "moderator_id",
2083
+ options
2084
+ });
2085
+ }
2086
+ shieldMode(params, options) {
2087
+ return this.rest.one({
2088
+ path: "moderation/shield_mode",
2089
+ query: params,
2090
+ scopes: ["moderator:read:shield_mode", "moderator:manage:shield_mode"],
2091
+ context: "moderator_id",
2092
+ options
2093
+ });
2094
+ }
2095
+ warn(params, options) {
2096
+ const { broadcaster_id, moderator_id, ...data } = params;
2097
+ return this.rest.one({
2098
+ method: "POST",
2099
+ path: "moderation/warnings",
2100
+ query: { broadcaster_id, moderator_id },
2101
+ body: { data },
2102
+ scopes: ["moderator:manage:warnings"],
2103
+ context: "moderator_id",
2104
+ options
2105
+ });
2106
+ }
2107
+ addSuspiciousStatus(params, options) {
2108
+ const { broadcaster_id, moderator_id, ...body } = params;
2109
+ return this.rest.one({
2110
+ method: "POST",
2111
+ path: "moderation/suspicious_users",
2112
+ query: { broadcaster_id, moderator_id },
2113
+ body,
2114
+ scopes: ["moderator:manage:suspicious_users"],
2115
+ context: "moderator_id",
2116
+ options
2117
+ });
2118
+ }
2119
+ removeSuspiciousStatus(params, options) {
2120
+ return this.rest.one({
2121
+ method: "DELETE",
2122
+ path: "moderation/suspicious_users",
2123
+ query: params,
2124
+ scopes: ["moderator:manage:suspicious_users"],
2125
+ context: "moderator_id",
2126
+ options
2127
+ });
2128
+ }
2129
+ };
2130
+
2131
+ // src/api/polls.ts
2132
+ var PollsApi = class extends ApiGroup {
2133
+ get(params = {}, options) {
2134
+ return this.rest.paginate(
2135
+ {
2136
+ path: "polls",
2137
+ query: params,
2138
+ auth: "user",
2139
+ scopes: ["channel:read:polls", "channel:manage:polls"],
2140
+ context: "broadcaster_id",
2141
+ options
2142
+ },
2143
+ { max: 20 }
2144
+ );
2145
+ }
2146
+ create(params, options) {
2147
+ return this.rest.one({
2148
+ method: "POST",
2149
+ path: "polls",
2150
+ body: {
2151
+ ...params,
2152
+ choices: params.choices.map((choice) => typeof choice === "string" ? { title: choice } : choice)
2153
+ },
2154
+ auth: "user",
2155
+ scopes: ["channel:manage:polls"],
2156
+ context: { body: "broadcaster_id" },
2157
+ options
2158
+ });
2159
+ }
2160
+ end(params, options) {
2161
+ return this.rest.one({
2162
+ method: "PATCH",
2163
+ path: "polls",
2164
+ body: params,
2165
+ auth: "user",
2166
+ scopes: ["channel:manage:polls"],
2167
+ context: { body: "broadcaster_id" },
2168
+ options
2169
+ });
2170
+ }
2171
+ };
2172
+
2173
+ // src/api/predictions.ts
2174
+ var PredictionsApi = class extends ApiGroup {
2175
+ get(params = {}, options) {
2176
+ return this.rest.paginate(
2177
+ {
2178
+ path: "predictions",
2179
+ query: params,
2180
+ auth: "user",
2181
+ scopes: ["channel:read:predictions", "channel:manage:predictions"],
2182
+ context: "broadcaster_id",
2183
+ options
2184
+ },
2185
+ { max: 25 }
2186
+ );
2187
+ }
2188
+ create(params, options) {
2189
+ return this.rest.one({
2190
+ method: "POST",
2191
+ path: "predictions",
2192
+ body: {
2193
+ ...params,
2194
+ outcomes: params.outcomes.map((outcome) => typeof outcome === "string" ? { title: outcome } : outcome)
2195
+ },
2196
+ auth: "user",
2197
+ scopes: ["channel:manage:predictions"],
2198
+ context: { body: "broadcaster_id" },
2199
+ options
2200
+ });
2201
+ }
2202
+ end(params, options) {
2203
+ return this.rest.one({
2204
+ method: "PATCH",
2205
+ path: "predictions",
2206
+ body: params,
2207
+ auth: "user",
2208
+ scopes: ["channel:manage:predictions"],
2209
+ context: { body: "broadcaster_id" },
2210
+ options
2211
+ });
2212
+ }
2213
+ };
2214
+
2215
+ // src/api/raids.ts
2216
+ var RaidsApi = class extends ApiGroup {
2217
+ start(params, options) {
2218
+ return this.rest.one({
2219
+ method: "POST",
2220
+ path: "raids",
2221
+ query: params,
2222
+ auth: "user",
2223
+ scopes: ["channel:manage:raids"],
2224
+ context: "from_broadcaster_id",
2225
+ options
2226
+ });
2227
+ }
2228
+ cancel(params = {}, options) {
2229
+ return this.rest.send({
2230
+ method: "DELETE",
2231
+ path: "raids",
2232
+ query: params,
2233
+ auth: "user",
2234
+ scopes: ["channel:manage:raids"],
2235
+ context: "broadcaster_id",
2236
+ options
2237
+ });
2238
+ }
2239
+ };
2240
+
2241
+ // src/api/schedule.ts
2242
+ var ScheduleApi = class extends ApiGroup {
2243
+ get(params, options) {
2244
+ return this.rest.paginate(
2245
+ { path: "schedule", query: params, options },
2246
+ { max: 25, extract: (body) => ({ data: body.data.segments, cursor: body.pagination?.cursor }) }
2247
+ );
2248
+ }
2249
+ icalendar(params, options) {
2250
+ return this.rest.request({ path: "schedule/icalendar", query: params, response: "text", options });
2251
+ }
2252
+ updateSettings(params = {}, options) {
2253
+ return this.rest.send({
2254
+ method: "PATCH",
2255
+ path: "schedule/settings",
2256
+ query: params,
2257
+ auth: "user",
2258
+ scopes: ["channel:manage:schedule"],
2259
+ context: "broadcaster_id",
2260
+ options
2261
+ });
2262
+ }
2263
+ async createSegment(params, options) {
2264
+ const { broadcaster_id, ...body } = params;
2265
+ const response = await this.rest.request({
2266
+ method: "POST",
2267
+ path: "schedule/segment",
2268
+ query: { broadcaster_id },
2269
+ body,
2270
+ auth: "user",
2271
+ scopes: ["channel:manage:schedule"],
2272
+ context: "broadcaster_id",
2273
+ options
2274
+ });
2275
+ return response.data;
2276
+ }
2277
+ async updateSegment(params, options) {
2278
+ const { broadcaster_id, id, ...body } = params;
2279
+ const response = await this.rest.request({
2280
+ method: "PATCH",
2281
+ path: "schedule/segment",
2282
+ query: { broadcaster_id, id },
2283
+ body,
2284
+ auth: "user",
2285
+ scopes: ["channel:manage:schedule"],
2286
+ context: "broadcaster_id",
2287
+ options
2288
+ });
2289
+ return response.data;
2290
+ }
2291
+ deleteSegment(params, options) {
2292
+ return this.rest.send({
2293
+ method: "DELETE",
2294
+ path: "schedule/segment",
2295
+ query: params,
2296
+ auth: "user",
2297
+ scopes: ["channel:manage:schedule"],
2298
+ context: "broadcaster_id",
2299
+ options
2300
+ });
2301
+ }
2302
+ };
2303
+
2304
+ // src/api/search.ts
2305
+ var SearchApi = class extends ApiGroup {
2306
+ categories(params, options) {
2307
+ return this.rest.paginate({ path: "search/categories", query: params, options });
2308
+ }
2309
+ channels(params, options) {
2310
+ return this.rest.paginate({ path: "search/channels", query: params, options });
2311
+ }
2312
+ };
2313
+
2314
+ // src/api/streams.ts
2315
+ var StreamsApi = class extends ApiGroup {
2316
+ get(params = {}, options) {
2317
+ return this.rest.paginate({ path: "streams", query: params, options });
2318
+ }
2319
+ followed(params = {}, options) {
2320
+ return this.rest.paginate({
2321
+ path: "streams/followed",
2322
+ query: params,
2323
+ auth: "user",
2324
+ scopes: ["user:read:follows"],
2325
+ context: "user_id",
2326
+ options
2327
+ });
2328
+ }
2329
+ async key(params = {}, options) {
2330
+ const { stream_key } = await this.rest.one({
2331
+ path: "streams/key",
2332
+ query: params,
2333
+ auth: "user",
2334
+ scopes: ["channel:read:stream_key"],
2335
+ context: "broadcaster_id",
2336
+ options
2337
+ });
2338
+ return stream_key;
2339
+ }
2340
+ createMarker(params = {}, options) {
2341
+ return this.rest.one({
2342
+ method: "POST",
2343
+ path: "streams/markers",
2344
+ body: params,
2345
+ auth: "user",
2346
+ scopes: ["channel:manage:broadcast"],
2347
+ context: { body: "user_id" },
2348
+ options
2349
+ });
2350
+ }
2351
+ markers(params, options) {
2352
+ return this.rest.paginate({
2353
+ path: "streams/markers",
2354
+ query: params,
2355
+ auth: "user",
2356
+ scopes: ["user:read:broadcast", "channel:manage:broadcast"],
2357
+ options
2358
+ });
2359
+ }
2360
+ };
2361
+
2362
+ // src/api/subscriptions.ts
2363
+ var SubscriptionsApi = class extends ApiGroup {
2364
+ get(params = {}, options) {
2365
+ return this.rest.paginate({
2366
+ path: "subscriptions",
2367
+ query: params,
2368
+ auth: "user",
2369
+ scopes: ["channel:read:subscriptions"],
2370
+ context: "broadcaster_id",
2371
+ options
2372
+ });
2373
+ }
2374
+ async check(params, options) {
2375
+ try {
2376
+ return await this.rest.first({
2377
+ path: "subscriptions/user",
2378
+ query: params,
2379
+ auth: "user",
2380
+ scopes: ["user:read:subscriptions"],
2381
+ context: "user_id",
2382
+ options
2383
+ });
2384
+ } catch (error) {
2385
+ if (error instanceof TwitchApiError && error.status === 404) return null;
2386
+ throw error;
2387
+ }
2388
+ }
2389
+ };
2390
+
2391
+ // src/api/tags.ts
2392
+ var TagsApi = class extends ApiGroup {
2393
+ all(params = {}, options) {
2394
+ return this.rest.paginate({ path: "tags/streams", query: params, options });
2395
+ }
2396
+ ofChannel(params, options) {
2397
+ return this.rest.data({ path: "streams/tags", query: params, options });
2398
+ }
2399
+ };
2400
+
2401
+ // src/api/teams.ts
2402
+ var TeamsApi = class extends ApiGroup {
2403
+ ofChannel(params, options) {
2404
+ return this.rest.data({ path: "teams/channel", query: params, options });
2405
+ }
2406
+ async get(params, options) {
2407
+ try {
2408
+ return await this.rest.first({ path: "teams", query: params, options });
2409
+ } catch (error) {
2410
+ if (error instanceof TwitchApiError && error.status === 404) return null;
2411
+ throw error;
2412
+ }
2413
+ }
2414
+ };
2415
+
2416
+ // src/http/batcher.ts
2417
+ var Batcher = class {
2418
+ #load;
2419
+ #max;
2420
+ #queue = /* @__PURE__ */ new Map();
2421
+ #scheduled = false;
2422
+ constructor(load, max = 100) {
2423
+ this.#load = load;
2424
+ this.#max = max;
2425
+ }
2426
+ get(key) {
2427
+ return new Promise((resolve, reject) => {
2428
+ const waiters = this.#queue.get(key);
2429
+ if (waiters) waiters.push({ resolve, reject });
2430
+ else this.#queue.set(key, [{ resolve, reject }]);
2431
+ if (!this.#scheduled) {
2432
+ this.#scheduled = true;
2433
+ queueMicrotask(() => this.#flush());
2434
+ }
2435
+ });
2436
+ }
2437
+ #flush() {
2438
+ const queue = this.#queue;
2439
+ this.#queue = /* @__PURE__ */ new Map();
2440
+ this.#scheduled = false;
2441
+ for (const keys of chunk([...queue.keys()], this.#max)) {
2442
+ this.#load(keys).then(
2443
+ (found) => {
2444
+ for (const key of keys) for (const waiter of queue.get(key)) waiter.resolve(found.get(key) ?? null);
2445
+ },
2446
+ (error) => {
2447
+ for (const key of keys) for (const waiter of queue.get(key)) waiter.reject(error);
2448
+ }
2449
+ );
2450
+ }
2451
+ }
2452
+ };
2453
+
2454
+ // src/api/users.ts
2455
+ var UsersApi = class extends ApiGroup {
2456
+ #byId;
2457
+ #byLogin;
2458
+ constructor(rest) {
2459
+ super(rest);
2460
+ this.#byId = new Batcher(async (ids) => new Map((await this.get({ id: ids })).map((user) => [user.id, user])));
2461
+ this.#byLogin = new Batcher(
2462
+ async (logins) => new Map((await this.get({ login: logins })).map((user) => [user.login, user]))
2463
+ );
2464
+ }
2465
+ get(params, options) {
2466
+ return this.rest.chunked({ path: "users", query: params, options }, ["id", "login"]);
2467
+ }
2468
+ async getById(id, options) {
2469
+ if (!options) return this.#byId.get(id);
2470
+ return (await this.get({ id }, options))[0] ?? null;
2471
+ }
2472
+ async getByLogin(login, options) {
2473
+ const key = login.toLowerCase();
2474
+ if (!options) return this.#byLogin.get(key);
2475
+ return (await this.get({ login: key }, options))[0] ?? null;
2476
+ }
2477
+ me(options) {
2478
+ return this.rest.one({ path: "users", auth: "user", options });
2479
+ }
2480
+ update(params, options) {
2481
+ return this.rest.one({
2482
+ method: "PUT",
2483
+ path: "users",
2484
+ query: params,
2485
+ auth: "user",
2486
+ scopes: ["user:edit"],
2487
+ options
2488
+ });
2489
+ }
2490
+ authorization(params, options) {
2491
+ return this.rest.chunked(
2492
+ { path: "authorization/users", query: params, auth: "app", options },
2493
+ ["user_id"],
2494
+ 10
2495
+ );
2496
+ }
2497
+ blocks(params = {}, options) {
2498
+ return this.rest.paginate({
2499
+ path: "users/blocks",
2500
+ query: params,
2501
+ auth: "user",
2502
+ scopes: ["user:read:blocked_users"],
2503
+ context: "broadcaster_id",
2504
+ options
2505
+ });
2506
+ }
2507
+ block(params, options) {
2508
+ return this.rest.send({
2509
+ method: "PUT",
2510
+ path: "users/blocks",
2511
+ query: params,
2512
+ auth: "user",
2513
+ scopes: ["user:manage:blocked_users"],
2514
+ options
2515
+ });
2516
+ }
2517
+ unblock(params, options) {
2518
+ return this.rest.send({
2519
+ method: "DELETE",
2520
+ path: "users/blocks",
2521
+ query: params,
2522
+ auth: "user",
2523
+ scopes: ["user:manage:blocked_users"],
2524
+ options
2525
+ });
2526
+ }
2527
+ extensions(options) {
2528
+ return this.rest.data({
2529
+ path: "users/extensions/list",
2530
+ auth: "user",
2531
+ scopes: ["user:read:broadcast", "user:edit:broadcast"],
2532
+ options
2533
+ });
2534
+ }
2535
+ async activeExtensions(params = {}, options) {
2536
+ const body = await this.rest.request({
2537
+ path: "users/extensions",
2538
+ query: params,
2539
+ options
2540
+ });
2541
+ return body.data;
2542
+ }
2543
+ async updateExtensions(params, options) {
2544
+ const body = await this.rest.request({
2545
+ method: "PUT",
2546
+ path: "users/extensions",
2547
+ body: { data: params.data },
2548
+ auth: "user",
2549
+ scopes: ["user:edit:broadcast"],
2550
+ options
2551
+ });
2552
+ return body.data;
2553
+ }
2554
+ };
2555
+
2556
+ // src/api/videos.ts
2557
+ var VideosApi = class extends ApiGroup {
2558
+ get(params, options) {
2559
+ return this.rest.paginate({ path: "videos", query: params, options });
2560
+ }
2561
+ async delete(params, options) {
2562
+ const deleted = [];
2563
+ for (const id of chunk(toArray(params.id), 5)) {
2564
+ const ids = await this.rest.data({
2565
+ method: "DELETE",
2566
+ path: "videos",
2567
+ query: { id },
2568
+ auth: "user",
2569
+ scopes: ["channel:manage:videos"],
2570
+ options
2571
+ });
2572
+ deleted.push(...ids);
2573
+ }
2574
+ return deleted;
2575
+ }
2576
+ };
2577
+
2578
+ // src/api/whispers.ts
2579
+ var WhispersApi = class extends ApiGroup {
2580
+ send(params, options) {
2581
+ const { message, ...query } = params;
2582
+ return this.rest.send({
2583
+ method: "POST",
2584
+ path: "whispers",
2585
+ query,
2586
+ body: { message },
2587
+ auth: "user",
2588
+ scopes: ["user:manage:whispers"],
2589
+ context: "from_user_id",
2590
+ options
2591
+ });
2592
+ }
2593
+ };
2594
+
2595
+ // src/auth/tokens.ts
2596
+ var MemoryTokenStore = class {
2597
+ #tokens = /* @__PURE__ */ new Map();
2598
+ get(userId) {
2599
+ return this.#tokens.get(userId);
2600
+ }
2601
+ set(userId, token) {
2602
+ this.#tokens.set(userId, token);
2603
+ }
2604
+ delete(userId) {
2605
+ this.#tokens.delete(userId);
2606
+ }
2607
+ list() {
2608
+ return [...this.#tokens.keys()];
2609
+ }
2610
+ };
2611
+
2612
+ // src/auth/manager.ts
2613
+ var EXPIRY_MARGIN = 6e4;
2614
+ var HOUR = 36e5;
2615
+ var Auth = class {
2616
+ clientId;
2617
+ #secret;
2618
+ #store;
2619
+ #hasStore;
2620
+ #fetch;
2621
+ #authUrl;
2622
+ #events;
2623
+ #validate;
2624
+ #timeout;
2625
+ #retries;
2626
+ #users = /* @__PURE__ */ new Map();
2627
+ #refreshing = /* @__PURE__ */ new Map();
2628
+ #removals = /* @__PURE__ */ new Map();
2629
+ #app;
2630
+ #appPending;
2631
+ #restored;
2632
+ #timer;
2633
+ constructor(config) {
2634
+ this.clientId = config.clientId;
2635
+ this.#secret = config.clientSecret;
2636
+ this.#hasStore = config.store !== void 0;
2637
+ this.#store = config.store ?? new MemoryTokenStore();
2638
+ this.#fetch = config.fetch;
2639
+ this.#authUrl = config.authUrl.replace(/\/+$/, "");
2640
+ this.#events = config.events;
2641
+ this.#validate = config.validate;
2642
+ this.#timeout = config.timeout;
2643
+ this.#retries = config.retries;
2644
+ }
2645
+ get hasSecret() {
2646
+ return this.#secret !== void 0;
2647
+ }
2648
+ get users() {
2649
+ return [...this.#users.values()];
2650
+ }
2651
+ getUser(userId) {
2652
+ return this.#users.get(userId);
2653
+ }
2654
+ hasUser(userId) {
2655
+ return this.#users.has(userId);
2656
+ }
2657
+ async addUser(input) {
2658
+ let accessToken = input.accessToken;
2659
+ let refreshToken = input.refreshToken;
2660
+ let expiresAt = input.expiresAt instanceof Date ? input.expiresAt.getTime() : input.expiresAt ?? (input.expiresIn ? Date.now() + input.expiresIn * 1e3 : void 0);
2661
+ let userId = input.userId;
2662
+ let login = input.login;
2663
+ let scopes = input.scopes ? [...input.scopes] : void 0;
2664
+ if (!userId || !scopes) {
2665
+ let info;
2666
+ try {
2667
+ info = await this.validate(accessToken);
2668
+ } catch (error) {
2669
+ if (!(error instanceof TwitchApiError) || error.status !== 401 || !refreshToken) throw error;
2670
+ const fresh = await this.#requestRefresh(refreshToken);
2671
+ accessToken = fresh.access_token;
2672
+ refreshToken = fresh.refresh_token ?? refreshToken;
2673
+ expiresAt = fresh.expires_in ? Date.now() + fresh.expires_in * 1e3 : void 0;
2674
+ info = await this.validate(accessToken);
2675
+ }
2676
+ if (!info.userId) {
2677
+ throw new TwitchAuthError("This is an app access token, not a user access token.");
2678
+ }
2679
+ if (info.clientId !== this.clientId) {
2680
+ throw new TwitchAuthError(
2681
+ `This token was issued to another application (client ID ${info.clientId}), so it cannot be used with client ID ${this.clientId}.`,
2682
+ { userId: info.userId }
2683
+ );
2684
+ }
2685
+ userId = info.userId;
2686
+ login = info.login ?? login;
2687
+ scopes = info.scopes;
2688
+ expiresAt ??= info.expiresAt;
2689
+ }
2690
+ const token = { userId, accessToken, scopes };
2691
+ if (login) token.login = login;
2692
+ if (refreshToken) token.refreshToken = refreshToken;
2693
+ if (expiresAt !== void 0) token.expiresAt = expiresAt;
2694
+ await this.#save(token);
2695
+ return token;
2696
+ }
2697
+ async removeUser(userId, options = {}) {
2698
+ const token = this.#users.get(userId);
2699
+ this.#removals.set(userId, (this.#removals.get(userId) ?? 0) + 1);
2700
+ this.#users.delete(userId);
2701
+ await this.#store.delete(userId);
2702
+ if (options.revoke && token) await this.revoke(token.accessToken).catch(() => {
2703
+ });
2704
+ if (this.#users.size === 0) this.#stopTimer();
2705
+ }
2706
+ restore() {
2707
+ return this.#restored ??= (async () => {
2708
+ const ids = await this.#store.list?.() ?? [];
2709
+ const loaded = [];
2710
+ for (const id of ids) {
2711
+ if (this.#users.has(id)) continue;
2712
+ const token = await this.#store.get(id);
2713
+ if (token) {
2714
+ this.#users.set(id, token);
2715
+ loaded.push(token);
2716
+ }
2717
+ }
2718
+ if (this.#users.size > 0) this.#startTimer();
2719
+ return loaded;
2720
+ })().catch((error) => {
2721
+ this.#restored = void 0;
2722
+ throw error;
2723
+ });
2724
+ }
2725
+ authorizeUrl(options) {
2726
+ const params = new URLSearchParams({
2727
+ response_type: options.responseType ?? "code",
2728
+ client_id: this.clientId,
2729
+ redirect_uri: options.redirectUri,
2730
+ scope: (options.scopes ?? []).join(" ")
2731
+ });
2732
+ if (options.state) params.set("state", options.state);
2733
+ if (options.forceVerify) params.set("force_verify", "true");
2734
+ return `${this.#authUrl}/authorize?${params}`;
2735
+ }
2736
+ async exchangeCode(code, redirectUri) {
2737
+ const response = await this.#form("/token", {
2738
+ client_id: this.clientId,
2739
+ client_secret: this.#requireSecret("exchange an authorization code"),
2740
+ code,
2741
+ grant_type: "authorization_code",
2742
+ redirect_uri: redirectUri
2743
+ });
2744
+ return this.#fromResponse(response);
2745
+ }
2746
+ async deviceFlow(options) {
2747
+ const scopes = options.scopes.join(" ");
2748
+ const start = await this.#form("/device", { client_id: this.clientId, scopes });
2749
+ const expiresAt = Date.now() + start.expires_in * 1e3;
2750
+ let interval = start.interval;
2751
+ const wait = async (signal) => {
2752
+ while (Date.now() < expiresAt) {
2753
+ await sleep(interval * 1e3, signal);
2754
+ try {
2755
+ const response = await this.#form("/token", {
2756
+ client_id: this.clientId,
2757
+ ...this.#secret ? { client_secret: this.#secret } : {},
2758
+ scopes,
2759
+ device_code: start.device_code,
2760
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
2761
+ });
2762
+ return await this.#fromResponse(response);
2763
+ } catch (error) {
2764
+ if (error instanceof TwitchNetworkError || error instanceof TwitchTimeoutError) continue;
2765
+ if (!(error instanceof TwitchApiError) || error.status !== 400) throw error;
2766
+ if (/slow_down/i.test(error.message)) interval += 5;
2767
+ else if (!/authorization_pending/i.test(error.message)) throw error;
2768
+ }
2769
+ }
2770
+ throw new TwitchAuthError("The device code expired before the user approved the request.");
2771
+ };
2772
+ return {
2773
+ userCode: start.user_code,
2774
+ verificationUri: start.verification_uri,
2775
+ deviceCode: start.device_code,
2776
+ expiresAt,
2777
+ get interval() {
2778
+ return interval;
2779
+ },
2780
+ wait
2781
+ };
2782
+ }
2783
+ refresh(userId) {
2784
+ const pending = this.#refreshing.get(userId);
2785
+ if (pending) return pending;
2786
+ const run = (async () => {
2787
+ const removals = this.#removals.get(userId) ?? 0;
2788
+ const memory = this.#users.get(userId);
2789
+ const stored = this.#hasStore ? await this.#store.get(userId) : void 0;
2790
+ if (memory && stored && stored.accessToken !== memory.accessToken) {
2791
+ this.#users.set(userId, stored);
2792
+ return stored;
2793
+ }
2794
+ const current = memory ?? stored ?? await this.#store.get(userId);
2795
+ if (!current) throw new TwitchAuthError(`No token is registered for user ${userId}.`, { userId });
2796
+ if (!current.refreshToken) {
2797
+ throw new TwitchAuthError(
2798
+ `The access token of user ${userId} expired and there is no refresh token to renew it.`,
2799
+ { userId }
2800
+ );
2801
+ }
2802
+ let response;
2803
+ try {
2804
+ response = await this.#requestRefresh(current.refreshToken);
2805
+ } catch (error) {
2806
+ if (error instanceof TwitchApiError && error.status < 500 && /invalid refresh token/i.test(error.message)) {
2807
+ const latest = this.#hasStore ? await this.#store.get(userId) : void 0;
2808
+ if (latest && latest.refreshToken !== current.refreshToken) {
2809
+ this.#users.set(userId, latest);
2810
+ return latest;
2811
+ }
2812
+ const dead = new TwitchAuthError(
2813
+ `Twitch rejected the refresh token of user ${userId}: ${error.message}. The user has to authorize the app again.`,
2814
+ { cause: error, userId }
2815
+ );
2816
+ await this.#drop(userId, dead);
2817
+ throw dead;
2818
+ }
2819
+ throw error;
2820
+ }
2821
+ const token = {
2822
+ ...current,
2823
+ accessToken: response.access_token,
2824
+ refreshToken: response.refresh_token ?? current.refreshToken,
2825
+ scopes: response.scope ?? current.scopes
2826
+ };
2827
+ if (response.expires_in) token.expiresAt = Date.now() + response.expires_in * 1e3;
2828
+ else delete token.expiresAt;
2829
+ if ((this.#removals.get(userId) ?? 0) !== removals) {
2830
+ throw new TwitchAuthError(`User ${userId} was removed while their token was being renewed.`, { userId });
2831
+ }
2832
+ await this.#save(token);
2833
+ this.#events.emit("tokenRefresh", token);
2834
+ return token;
2835
+ })().finally(() => this.#refreshing.delete(userId));
2836
+ this.#refreshing.set(userId, run);
2837
+ return run;
2838
+ }
2839
+ async validate(token) {
2840
+ const url = `${this.#authUrl}/validate`;
2841
+ const response = await this.#send("GET", url, { headers: { Authorization: `OAuth ${token}` } }, true);
2842
+ const body = await response.json().catch(() => void 0);
2843
+ if (!response.ok || !body) {
2844
+ throw new TwitchApiError(body?.message ?? `Token validation failed with status ${response.status}`, {
2845
+ status: response.status,
2846
+ method: "GET",
2847
+ url,
2848
+ body,
2849
+ headers: response.headers
2850
+ });
2851
+ }
2852
+ const info = {
2853
+ clientId: body.client_id,
2854
+ userId: body.user_id || null,
2855
+ login: body.login || null,
2856
+ scopes: body.scopes ?? [],
2857
+ expiresIn: body.expires_in ?? 0
2858
+ };
2859
+ if (body.expires_in) info.expiresAt = Date.now() + body.expires_in * 1e3;
2860
+ return info;
2861
+ }
2862
+ async revoke(token) {
2863
+ await this.#form("/revoke", { client_id: this.clientId, token });
2864
+ }
2865
+ appToken() {
2866
+ if (this.#app && this.#app.expiresAt - EXPIRY_MARGIN > Date.now()) return Promise.resolve(this.#app.token);
2867
+ return this.#appPending ??= (async () => {
2868
+ const response = await this.#form(
2869
+ "/token",
2870
+ {
2871
+ client_id: this.clientId,
2872
+ client_secret: this.#requireSecret("get an app access token"),
2873
+ grant_type: "client_credentials"
2874
+ },
2875
+ true
2876
+ );
2877
+ this.#app = {
2878
+ token: response.access_token,
2879
+ expiresAt: Date.now() + (response.expires_in ?? 3600) * 1e3
2880
+ };
2881
+ return response.access_token;
2882
+ })().finally(() => {
2883
+ this.#appPending = void 0;
2884
+ });
2885
+ }
2886
+ async validateAll() {
2887
+ await Promise.all(
2888
+ [...this.#users.values()].map(async (user) => {
2889
+ try {
2890
+ const info = await this.validate(user.accessToken);
2891
+ const current = this.#users.get(user.userId);
2892
+ if (!current || current.accessToken !== user.accessToken) return;
2893
+ current.scopes = info.scopes;
2894
+ if (info.expiresAt !== void 0) current.expiresAt = info.expiresAt;
2895
+ } catch (error) {
2896
+ if (!(error instanceof TwitchApiError) || error.status !== 401) return;
2897
+ if (user.refreshToken) await this.refresh(user.userId).catch(() => {
2898
+ });
2899
+ else await this.#drop(user.userId, new TwitchAuthError(`The token of user ${user.userId} is no longer valid.`, { cause: error, userId: user.userId }));
2900
+ }
2901
+ })
2902
+ );
2903
+ }
2904
+ close() {
2905
+ this.#stopTimer();
2906
+ }
2907
+ async resolve(need) {
2908
+ if (need.token) return { token: need.token, kind: "raw", bucket: `raw:${fingerprint(need.token)}` };
2909
+ if (need.as === "app") {
2910
+ if (need.auth === "user") {
2911
+ throw new TwitchAuthError(`${need.what} only accepts a user access token: the app token cannot be used for it.`);
2912
+ }
2913
+ return { token: await this.appToken(), kind: "app", bucket: "app" };
2914
+ }
2915
+ if (this.#hasStore) await this.restore();
2916
+ if (need.as && need.auth !== "app") return this.#userGrant(need.as, need);
2917
+ if (need.auth !== "app") {
2918
+ if (need.contextId && await this.#known(need.contextId)) return this.#userGrant(need.contextId, need);
2919
+ if (need.auth === "user") {
2920
+ const sole = this.#sole();
2921
+ if (sole) return this.#userGrant(sole, need);
2922
+ throw new TwitchAuthError(
2923
+ this.#users.size === 0 ? `${need.what} needs a user access token and no user is registered. Add one with twitch.auth.addUser().` : need.contextId ? `${need.what} needs the token of user ${need.contextId}, which is not registered. Add it with twitch.auth.addUser() or choose a user with { as: userId }.` : `${need.what} needs a user access token and several users are registered. Choose one with { as: userId } or twitch.as(userId).`
2924
+ );
2925
+ }
2926
+ if (need.contextMissing || need.preferUser) {
2927
+ const sole = this.#sole();
2928
+ if (sole) return this.#userGrant(sole, need);
2929
+ }
2930
+ }
2931
+ if (this.#secret) return { token: await this.appToken(), kind: "app", bucket: "app" };
2932
+ if (need.auth === "any") {
2933
+ const sole = this.#sole();
2934
+ if (sole) return this.#userGrant(sole, need);
2935
+ }
2936
+ if (need.auth === "any" && this.#users.size > 1) {
2937
+ throw new TwitchAuthError(
2938
+ `${need.what} can use a user token, but several users are registered. Choose one with { as: userId } or twitch.as(userId).`
2939
+ );
2940
+ }
2941
+ throw new TwitchAuthError(
2942
+ `${need.what} needs an app access token, which requires a clientSecret${need.auth === "any" ? " (or a registered user token)" : ""}.`
2943
+ );
2944
+ }
2945
+ async renew(grant) {
2946
+ if (grant.kind === "raw") return null;
2947
+ if (grant.kind === "app" && this.#app && this.#app.token !== grant.token) {
2948
+ return { ...grant, token: this.#app.token };
2949
+ }
2950
+ const user = grant.userId ? this.#users.get(grant.userId) : void 0;
2951
+ if (user && user.accessToken !== grant.token) return { ...grant, token: user.accessToken };
2952
+ try {
2953
+ await this.validate(grant.token);
2954
+ return null;
2955
+ } catch (error) {
2956
+ if (!(error instanceof TwitchApiError) || error.status !== 401) return null;
2957
+ }
2958
+ if (grant.kind === "app") {
2959
+ if (this.#app?.token === grant.token) this.#app = void 0;
2960
+ return { ...grant, token: await this.appToken() };
2961
+ }
2962
+ const userId = grant.userId;
2963
+ if (user && !user.refreshToken) {
2964
+ const dead = new TwitchAuthError(
2965
+ `The access token of user ${userId} is no longer valid and there is no refresh token to renew it. The user has to authorize the app again.`,
2966
+ { userId }
2967
+ );
2968
+ await this.#drop(userId, dead);
2969
+ throw dead;
2970
+ }
2971
+ return { ...grant, token: (await this.refresh(userId)).accessToken };
2972
+ }
2973
+ async #userGrant(userId, need) {
2974
+ let token = this.#users.get(userId);
2975
+ if (!token) {
2976
+ if (!await this.#known(userId)) {
2977
+ throw new TwitchAuthError(
2978
+ `No token is registered for user ${userId}. Add it with twitch.auth.addUser().`,
2979
+ { userId }
2980
+ );
2981
+ }
2982
+ token = this.#users.get(userId);
2983
+ }
2984
+ if (token.expiresAt !== void 0 && token.expiresAt - EXPIRY_MARGIN <= Date.now() && token.refreshToken) {
2985
+ token = await this.refresh(userId);
2986
+ }
2987
+ if (need.scopes?.length && !need.scopes.some((scope) => token.scopes.includes(scope))) {
2988
+ throw new TwitchScopeError(userId, need.scopes, token.scopes, need.what);
2989
+ }
2990
+ return { token: token.accessToken, kind: "user", userId, bucket: `user:${userId}` };
2991
+ }
2992
+ async #known(userId) {
2993
+ if (this.#users.has(userId)) return true;
2994
+ if (!this.#hasStore) return false;
2995
+ const token = await this.#store.get(userId);
2996
+ if (!token) return false;
2997
+ this.#users.set(userId, token);
2998
+ this.#startTimer();
2999
+ return true;
3000
+ }
3001
+ #sole() {
3002
+ if (this.#users.size !== 1) return void 0;
3003
+ return this.#users.keys().next().value;
3004
+ }
3005
+ async #save(token) {
3006
+ this.#users.set(token.userId, token);
3007
+ await this.#store.set(token.userId, token);
3008
+ this.#startTimer();
3009
+ }
3010
+ async #drop(userId, error) {
3011
+ this.#users.delete(userId);
3012
+ await Promise.resolve(this.#store.delete(userId)).catch(() => {
3013
+ });
3014
+ if (this.#users.size === 0) this.#stopTimer();
3015
+ this.#events.emit("tokenInvalid", userId, error);
3016
+ }
3017
+ async #fromResponse(response) {
3018
+ const input = { accessToken: response.access_token };
3019
+ if (response.refresh_token) input.refreshToken = response.refresh_token;
3020
+ if (response.expires_in) input.expiresIn = response.expires_in;
3021
+ return this.addUser(input);
3022
+ }
3023
+ #requestRefresh(refreshToken) {
3024
+ return this.#form("/token", {
3025
+ client_id: this.clientId,
3026
+ ...this.#secret ? { client_secret: this.#secret } : {},
3027
+ grant_type: "refresh_token",
3028
+ refresh_token: refreshToken
3029
+ });
3030
+ }
3031
+ #requireSecret(action) {
3032
+ if (!this.#secret) throw new TwitchAuthError(`A clientSecret is required to ${action}.`);
3033
+ return this.#secret;
3034
+ }
3035
+ async #form(path, params, repeatable = false) {
3036
+ const url = `${this.#authUrl}${path}`;
3037
+ const response = await this.#send(
3038
+ "POST",
3039
+ url,
3040
+ { headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(params).toString() },
3041
+ repeatable
3042
+ );
3043
+ const text = await response.text();
3044
+ let body;
3045
+ try {
3046
+ body = text ? JSON.parse(text) : void 0;
3047
+ } catch {
3048
+ body = text;
3049
+ }
3050
+ if (!response.ok) {
3051
+ const message = body?.message ?? `OAuth request failed with status ${response.status}`;
3052
+ throw new TwitchApiError(message, {
3053
+ status: response.status,
3054
+ method: "POST",
3055
+ url,
3056
+ body,
3057
+ headers: response.headers
3058
+ });
3059
+ }
3060
+ return body;
3061
+ }
3062
+ async #send(method, url, init, repeatable) {
3063
+ const what = `${method} ${new URL(url).pathname}`;
3064
+ for (let attempt = 0; ; attempt++) {
3065
+ const again = repeatable && attempt < this.#retries;
3066
+ try {
3067
+ const response = await this.#fetch(url, { ...init, method, signal: AbortSignal.timeout(this.#timeout) });
3068
+ if (response.status < 500 || !again) return response;
3069
+ } catch (error) {
3070
+ if (!again) {
3071
+ if (error instanceof Error && error.name === "TimeoutError") {
3072
+ throw new TwitchTimeoutError(`${what} timed out after ${this.#timeout} ms.`, { cause: error });
3073
+ }
3074
+ throw new TwitchNetworkError(`${what} could not reach Twitch: ${error.message}`, { cause: error });
3075
+ }
3076
+ }
3077
+ await sleep(backoff(attempt));
3078
+ }
3079
+ }
3080
+ #startTimer() {
3081
+ if (!this.#validate || this.#timer) return;
3082
+ this.#timer = unref(setInterval(() => void this.validateAll(), HOUR));
3083
+ }
3084
+ #stopTimer() {
3085
+ if (this.#timer) clearInterval(this.#timer);
3086
+ this.#timer = void 0;
3087
+ }
3088
+ };
3089
+
3090
+ // src/http/rate-limit.ts
3091
+ var RateLimiter = class {
3092
+ #buckets = /* @__PURE__ */ new Map();
3093
+ #events;
3094
+ constructor(events) {
3095
+ this.#events = events;
3096
+ }
3097
+ get(bucket) {
3098
+ return this.#buckets.get(bucket);
3099
+ }
3100
+ reserve(bucket) {
3101
+ const state = this.#buckets.get(bucket);
3102
+ if (!state) return 0;
3103
+ const now = Date.now();
3104
+ if (now >= state.resetAt) {
3105
+ this.#buckets.delete(bucket);
3106
+ return 0;
3107
+ }
3108
+ if (state.remaining > 0) {
3109
+ state.remaining--;
3110
+ return 0;
3111
+ }
3112
+ return state.resetAt - now;
3113
+ }
3114
+ async acquire(bucket, signal) {
3115
+ for (; ; ) {
3116
+ const waitMs = this.reserve(bucket);
3117
+ if (waitMs <= 0) return;
3118
+ this.#events.emit("rateLimit", { bucket, waitMs, resetAt: Date.now() + waitMs });
3119
+ await sleep(waitMs + 25, signal);
3120
+ }
3121
+ }
3122
+ update(bucket, headers) {
3123
+ const limit = Number(headers.get("ratelimit-limit"));
3124
+ const remainingHeader = headers.get("ratelimit-remaining");
3125
+ const reset = Number(headers.get("ratelimit-reset"));
3126
+ if (!limit || remainingHeader === null || !reset) return void 0;
3127
+ const remaining = Number(remainingHeader);
3128
+ const resetAt = reset * 1e3;
3129
+ const state = this.#buckets.get(bucket);
3130
+ if (state && state.resetAt === resetAt) {
3131
+ state.limit = limit;
3132
+ state.remaining = Math.min(state.remaining, remaining);
3133
+ } else {
3134
+ this.#buckets.set(bucket, { limit, remaining, resetAt });
3135
+ }
3136
+ return remaining;
3137
+ }
3138
+ exhaust(bucket, resetAt) {
3139
+ const state = this.#buckets.get(bucket);
3140
+ if (state) {
3141
+ state.remaining = 0;
3142
+ state.resetAt = Math.max(state.resetAt, resetAt);
3143
+ } else {
3144
+ this.#buckets.set(bucket, { limit: 0, remaining: 0, resetAt });
3145
+ }
3146
+ }
3147
+ };
3148
+
3149
+ // src/http/pagination.ts
3150
+ var Page = class _Page {
3151
+ data;
3152
+ cursor;
3153
+ total;
3154
+ raw;
3155
+ #fetch;
3156
+ #size;
3157
+ constructor(result, fetch, size) {
3158
+ this.data = result.data;
3159
+ this.cursor = result.cursor;
3160
+ this.total = result.total;
3161
+ this.raw = result.raw;
3162
+ this.#fetch = fetch;
3163
+ this.#size = size;
3164
+ }
3165
+ get hasNext() {
3166
+ return this.cursor !== void 0;
3167
+ }
3168
+ async next() {
3169
+ if (this.cursor === void 0) return null;
3170
+ return new _Page(await this.#fetch(this.cursor, this.#size), this.#fetch, this.#size);
3171
+ }
3172
+ [Symbol.iterator]() {
3173
+ return this.data[Symbol.iterator]();
3174
+ }
3175
+ };
3176
+ var Paginator = class {
3177
+ #fetch;
3178
+ #size;
3179
+ #maxSize;
3180
+ #first;
3181
+ constructor(fetch, size, maxSize) {
3182
+ this.#fetch = fetch;
3183
+ this.#size = size;
3184
+ this.#maxSize = maxSize;
3185
+ }
3186
+ then(onfulfilled, onrejected) {
3187
+ this.#first ??= this.#fetch(void 0, this.#size).then((result) => new Page(result, this.#fetch, this.#size));
3188
+ return this.#first.then(onfulfilled, onrejected);
3189
+ }
3190
+ async *pages() {
3191
+ const size = this.#size ?? this.#maxSize;
3192
+ let page = new Page(await this.#fetch(void 0, size), this.#fetch, size);
3193
+ const seen = /* @__PURE__ */ new Set();
3194
+ while (page) {
3195
+ yield page;
3196
+ if (page.cursor === void 0 || seen.has(page.cursor)) return;
3197
+ seen.add(page.cursor);
3198
+ page = await page.next();
3199
+ }
3200
+ }
3201
+ async *[Symbol.asyncIterator]() {
3202
+ for await (const page of this.pages()) yield* page.data;
3203
+ }
3204
+ async all(options = {}) {
3205
+ const limit = options.limit ?? Infinity;
3206
+ const items = [];
3207
+ if (limit <= 0) return items;
3208
+ const size = this.#size ?? Math.min(this.#maxSize, limit);
3209
+ let cursor;
3210
+ const seen = /* @__PURE__ */ new Set();
3211
+ for (; ; ) {
3212
+ const result = await this.#fetch(cursor, size);
3213
+ for (const item of result.data) {
3214
+ items.push(item);
3215
+ if (items.length >= limit) return items;
3216
+ }
3217
+ if (result.cursor === void 0 || seen.has(result.cursor)) return items;
3218
+ seen.add(result.cursor);
3219
+ cursor = result.cursor;
3220
+ }
3221
+ }
3222
+ take(count) {
3223
+ return this.all({ limit: count });
3224
+ }
3225
+ async first() {
3226
+ const size = this.#size ?? 1;
3227
+ return (await this.#fetch(void 0, size)).data[0];
3228
+ }
3229
+ async total() {
3230
+ return (await this.#fetch(void 0, 1)).total;
3231
+ }
3232
+ };
3233
+
3234
+ // src/http/rest.ts
3235
+ function buildQuery(query) {
3236
+ if (!query) return "";
3237
+ const params = new URLSearchParams();
3238
+ for (const [key, value] of Object.entries(query)) {
3239
+ if (value === void 0 || value === null) continue;
3240
+ if (Array.isArray(value)) {
3241
+ for (const item of value) if (item !== void 0 && item !== null) params.append(key, format(item));
3242
+ } else {
3243
+ params.append(key, format(value));
3244
+ }
3245
+ }
3246
+ const text = params.toString();
3247
+ return text ? `?${text}` : "";
3248
+ }
3249
+ function format(value) {
3250
+ return value instanceof Date ? value.toISOString() : String(value);
3251
+ }
3252
+ var IDEMPOTENT = /* @__PURE__ */ new Set(["GET", "PUT", "DELETE"]);
3253
+ var Rest = class _Rest {
3254
+ #config;
3255
+ #as;
3256
+ constructor(config, as) {
3257
+ this.#config = config;
3258
+ this.#as = as;
3259
+ }
3260
+ onClose(close) {
3261
+ this.#config.closers.add(close);
3262
+ }
3263
+ withUser(userId) {
3264
+ return new _Rest(this.#config, userId);
3265
+ }
3266
+ async request(endpoint) {
3267
+ const config = this.#config;
3268
+ const method = endpoint.method ?? "GET";
3269
+ const options = endpoint.options ?? {};
3270
+ const what = `${method} /${endpoint.path}`;
3271
+ const query = { ...endpoint.query };
3272
+ const body = endpoint.body ? { ...endpoint.body } : void 0;
3273
+ const context = endpoint.context;
3274
+ const contextKey = typeof context === "string" ? context : context?.body;
3275
+ const contextBag = typeof context === "string" ? query : body;
3276
+ const contextValue = contextKey ? contextBag?.[contextKey] : void 0;
3277
+ let grant = await config.auth.resolve({
3278
+ auth: endpoint.auth ?? "any",
3279
+ scopes: endpoint.scopes,
3280
+ as: options.as ?? this.#as,
3281
+ token: options.token,
3282
+ contextId: typeof contextValue === "string" ? contextValue : void 0,
3283
+ contextMissing: contextKey !== void 0 && (contextValue === void 0 || contextValue === null),
3284
+ preferUser: endpoint.prefer === "user",
3285
+ what
3286
+ });
3287
+ if (contextKey && contextBag && (contextValue === void 0 || contextValue === null)) {
3288
+ if (grant.userId) contextBag[contextKey] = grant.userId;
3289
+ else if (grant.kind === "raw") {
3290
+ throw new TwitchAuthError(`${what} needs "${contextKey}": a token passed by hand does not say who it belongs to.`);
3291
+ } else if (config.auth.users.length > 1) {
3292
+ throw new TwitchAuthError(
3293
+ `${what} does not say who is acting and several users are registered. Set "${contextKey}", or choose a user with { as: userId } or twitch.as(userId).`
3294
+ );
3295
+ } else {
3296
+ throw new TwitchAuthError(
3297
+ `${what} needs "${contextKey}": the call is made with the app token, which belongs to no user. Set "${contextKey}", or register the acting user with twitch.auth.addUser().`
3298
+ );
3299
+ }
3300
+ }
3301
+ const url = `${config.baseUrl}/${endpoint.path}${buildQuery(query)}`;
3302
+ const payload = body === void 0 ? void 0 : JSON.stringify(body);
3303
+ const retries = options.retries ?? config.retries;
3304
+ const timeout = options.timeout ?? config.timeout;
3305
+ let renewed = false;
3306
+ for (let attempt = 0; ; attempt++) {
3307
+ options.signal?.throwIfAborted();
3308
+ if (config.rateLimit === "wait") await config.limiter.acquire(grant.bucket, options.signal);
3309
+ else {
3310
+ const waitMs = config.limiter.reserve(grant.bucket);
3311
+ if (waitMs > 0) {
3312
+ throw new TwitchRateLimitError(
3313
+ `Rate limit reached for ${what}.`,
3314
+ { status: 429, method, url },
3315
+ Date.now() + waitMs
3316
+ );
3317
+ }
3318
+ }
3319
+ const headers = {
3320
+ Authorization: `Bearer ${grant.token}`,
3321
+ "Client-Id": config.auth.clientId
3322
+ };
3323
+ if (payload !== void 0) headers["Content-Type"] = "application/json";
3324
+ if (config.userAgent) headers["User-Agent"] = config.userAgent;
3325
+ const as = grant.kind === "user" ? grant.userId : grant.kind;
3326
+ const started = Date.now();
3327
+ config.events.emit("request", { method, url, attempt: attempt + 1, as });
3328
+ let response;
3329
+ let text;
3330
+ try {
3331
+ const signals = [AbortSignal.timeout(timeout)];
3332
+ if (options.signal) signals.push(options.signal);
3333
+ const init = { method, headers, signal: AbortSignal.any(signals) };
3334
+ if (payload !== void 0) init.body = payload;
3335
+ response = await config.fetch(url, init);
3336
+ text = response.status === 204 ? "" : await response.text();
3337
+ } catch (error) {
3338
+ if (options.signal?.aborted) throw options.signal.reason;
3339
+ const timedOut = error instanceof Error && error.name === "TimeoutError";
3340
+ if (attempt < retries && IDEMPOTENT.has(method)) {
3341
+ await sleep(backoff(attempt), options.signal);
3342
+ continue;
3343
+ }
3344
+ if (timedOut) throw new TwitchTimeoutError(`${what} timed out after ${timeout} ms.`, { cause: error });
3345
+ throw new TwitchNetworkError(`${what} could not reach Twitch: ${error.message}`, { cause: error });
3346
+ }
3347
+ const remaining = config.limiter.update(grant.bucket, response.headers);
3348
+ const event = {
3349
+ method,
3350
+ url,
3351
+ attempt: attempt + 1,
3352
+ as,
3353
+ status: response.status,
3354
+ duration: Date.now() - started
3355
+ };
3356
+ if (remaining !== void 0) event.remaining = remaining;
3357
+ config.events.emit("response", event);
3358
+ if (response.ok) {
3359
+ if (endpoint.response === "text") return text;
3360
+ if (!text) return void 0;
3361
+ try {
3362
+ return JSON.parse(text);
3363
+ } catch (error) {
3364
+ throw new TwitchError(`${what} answered with a body that is not JSON.`, { cause: error });
3365
+ }
3366
+ }
3367
+ const failure = readError(response, text, method, url);
3368
+ if (response.status === 401 && !renewed) {
3369
+ renewed = true;
3370
+ const fresh = await config.auth.renew(grant);
3371
+ if (fresh) {
3372
+ grant = fresh;
3373
+ attempt--;
3374
+ continue;
3375
+ }
3376
+ }
3377
+ if (response.status === 429) {
3378
+ const reset = Number(response.headers.get("ratelimit-reset"));
3379
+ const bucketEmpty = response.headers.get("ratelimit-remaining") === "0" && reset > 0;
3380
+ const resetAt = bucketEmpty ? reset * 1e3 : Date.now() + backoff(attempt, 1e3, 3e4);
3381
+ if (bucketEmpty) config.limiter.exhaust(grant.bucket, resetAt);
3382
+ if (config.rateLimit === "wait" && attempt < retries) {
3383
+ if (!bucketEmpty) {
3384
+ config.events.emit("rateLimit", { bucket: grant.bucket, waitMs: resetAt - Date.now(), resetAt });
3385
+ await sleep(Math.max(0, resetAt - Date.now()), options.signal);
3386
+ }
3387
+ continue;
3388
+ }
3389
+ throw new TwitchRateLimitError(failure.message, failure.info, resetAt);
3390
+ }
3391
+ if (response.status >= 500 && attempt < retries && IDEMPOTENT.has(method)) {
3392
+ await sleep(backoff(attempt), options.signal);
3393
+ continue;
3394
+ }
3395
+ throw new TwitchApiError(failure.message, failure.info);
3396
+ }
3397
+ }
3398
+ async data(endpoint) {
3399
+ const body = await this.request(endpoint);
3400
+ return body?.data ?? [];
3401
+ }
3402
+ async one(endpoint) {
3403
+ const [item] = await this.data(endpoint);
3404
+ if (item === void 0) {
3405
+ throw new TwitchError(`${endpoint.method ?? "GET"} /${endpoint.path} returned no data.`);
3406
+ }
3407
+ return item;
3408
+ }
3409
+ async first(endpoint) {
3410
+ const [item] = await this.data(endpoint);
3411
+ return item ?? null;
3412
+ }
3413
+ async send(endpoint) {
3414
+ await this.request(endpoint);
3415
+ }
3416
+ async chunked(endpoint, keys, size = 100) {
3417
+ const query = { ...endpoint.query };
3418
+ const pairs = [];
3419
+ for (const key of keys) {
3420
+ const value = query[key];
3421
+ if (value === void 0 || value === null) continue;
3422
+ for (const item of Array.isArray(value) ? value : [value]) pairs.push([key, item]);
3423
+ delete query[key];
3424
+ }
3425
+ if (pairs.length <= size) return this.data(endpoint);
3426
+ const results = await Promise.all(
3427
+ chunk(pairs, size).map((group) => {
3428
+ const part = { ...query };
3429
+ for (const [key, value] of group) (part[key] ??= []).push(value);
3430
+ return this.data({ ...endpoint, query: part });
3431
+ })
3432
+ );
3433
+ return results.flat();
3434
+ }
3435
+ paginate(endpoint, config = {}) {
3436
+ const query = endpoint.query ?? {};
3437
+ const sizeParam = config.sizeParam === void 0 ? "first" : config.sizeParam;
3438
+ const backward = query["before"] !== void 0 && query["before"] !== null;
3439
+ const cursorParam = backward ? "before" : "after";
3440
+ const size = sizeParam !== null && typeof query[sizeParam] === "number" ? query[sizeParam] : void 0;
3441
+ const extract = config.extract ?? defaultExtract;
3442
+ const fetchPage = async (cursor, pageSize) => {
3443
+ const next = { ...query };
3444
+ if (cursor !== void 0) next[cursorParam] = cursor;
3445
+ if (pageSize !== void 0 && sizeParam !== null) next[sizeParam] = pageSize;
3446
+ const raw = await this.request({ ...endpoint, query: next });
3447
+ const { data, cursor: nextCursor, total } = extract(raw);
3448
+ return { data: data ?? [], cursor: nextCursor || void 0, total, raw };
3449
+ };
3450
+ return new Paginator(fetchPage, size, config.max ?? 100);
3451
+ }
3452
+ };
3453
+ function defaultExtract(body) {
3454
+ const shape = body ?? {};
3455
+ const cursor = typeof shape.pagination === "string" ? shape.pagination : shape.pagination?.cursor;
3456
+ return { data: shape.data, cursor, total: shape.total ?? void 0 };
3457
+ }
3458
+ function readError(response, text, method, url) {
3459
+ let body = text;
3460
+ try {
3461
+ body = text ? JSON.parse(text) : void 0;
3462
+ } catch {
3463
+ }
3464
+ const shape = typeof body === "object" && body !== null ? body : {};
3465
+ const message = shape.message || shape.error || typeof body === "string" && body || response.statusText || "Request failed";
3466
+ const info = {
3467
+ status: response.status,
3468
+ method,
3469
+ url,
3470
+ body,
3471
+ headers: response.headers
3472
+ };
3473
+ if (shape.error) info.error = shape.error;
3474
+ return { message: `${message} (${response.status} on ${method} ${new URL(url).pathname})`, info };
3475
+ }
3476
+
3477
+ // src/client.ts
3478
+ var Twitch = class _Twitch {
3479
+ #shared;
3480
+ #rest;
3481
+ #groups = /* @__PURE__ */ new Map();
3482
+ userId;
3483
+ constructor(options, shared, userId) {
3484
+ if (shared) {
3485
+ this.#shared = shared;
3486
+ this.#rest = new Rest(shared.config, userId);
3487
+ this.userId = userId;
3488
+ return;
3489
+ }
3490
+ if (!options || typeof options.clientId !== "string" || options.clientId === "") {
3491
+ throw new TwitchError("A clientId is required: new Twitch({ clientId, clientSecret }).");
3492
+ }
3493
+ if (typeof (options.fetch ?? globalThis.fetch) !== "function") {
3494
+ throw new TwitchError("No fetch implementation found. Pass one with the `fetch` option.");
3495
+ }
3496
+ const doFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
3497
+ const events = new Emitter();
3498
+ const auth = new Auth({
3499
+ clientId: options.clientId,
3500
+ clientSecret: options.clientSecret,
3501
+ store: options.tokens,
3502
+ fetch: doFetch,
3503
+ authUrl: options.authUrl ?? "https://id.twitch.tv/oauth2",
3504
+ events,
3505
+ validate: options.validateTokens ?? true,
3506
+ timeout: options.timeout ?? 15e3,
3507
+ retries: options.retries ?? 3
3508
+ });
3509
+ const limiter = new RateLimiter(events);
3510
+ const config = {
3511
+ auth,
3512
+ limiter,
3513
+ events,
3514
+ fetch: doFetch,
3515
+ baseUrl: (options.baseUrl ?? "https://api.twitch.tv/helix").replace(/\/+$/, ""),
3516
+ timeout: options.timeout ?? 15e3,
3517
+ retries: options.retries ?? 3,
3518
+ rateLimit: options.rateLimit ?? "wait",
3519
+ userAgent: options.userAgent,
3520
+ closers: /* @__PURE__ */ new Set()
3521
+ };
3522
+ this.#shared = { auth, events, limiter, config };
3523
+ this.#rest = new Rest(config);
3524
+ this.userId = void 0;
3525
+ }
3526
+ get auth() {
3527
+ return this.#shared.auth;
3528
+ }
3529
+ get ads() {
3530
+ return this.#group(AdsApi);
3531
+ }
3532
+ get analytics() {
3533
+ return this.#group(AnalyticsApi);
3534
+ }
3535
+ get bits() {
3536
+ return this.#group(BitsApi);
3537
+ }
3538
+ get channels() {
3539
+ return this.#group(ChannelsApi);
3540
+ }
3541
+ get channelPoints() {
3542
+ return this.#group(ChannelPointsApi);
3543
+ }
3544
+ get charity() {
3545
+ return this.#group(CharityApi);
3546
+ }
3547
+ get chat() {
3548
+ return this.#group(ChatApi);
3549
+ }
3550
+ get clips() {
3551
+ return this.#group(ClipsApi);
3552
+ }
3553
+ get conduits() {
3554
+ return this.#group(ConduitsApi);
3555
+ }
3556
+ get contentLabels() {
3557
+ return this.#group(ContentLabelsApi);
3558
+ }
3559
+ get entitlements() {
3560
+ return this.#group(EntitlementsApi);
3561
+ }
3562
+ get extensions() {
3563
+ return this.#group(ExtensionsApi);
3564
+ }
3565
+ get eventsub() {
3566
+ return this.#group(EventSubApi);
3567
+ }
3568
+ get games() {
3569
+ return this.#group(GamesApi);
3570
+ }
3571
+ get goals() {
3572
+ return this.#group(GoalsApi);
3573
+ }
3574
+ get guestStar() {
3575
+ return this.#group(GuestStarApi);
3576
+ }
3577
+ get hypeTrain() {
3578
+ return this.#group(HypeTrainApi);
3579
+ }
3580
+ get moderation() {
3581
+ return this.#group(ModerationApi);
3582
+ }
3583
+ get polls() {
3584
+ return this.#group(PollsApi);
3585
+ }
3586
+ get predictions() {
3587
+ return this.#group(PredictionsApi);
3588
+ }
3589
+ get raids() {
3590
+ return this.#group(RaidsApi);
3591
+ }
3592
+ get schedule() {
3593
+ return this.#group(ScheduleApi);
3594
+ }
3595
+ get search() {
3596
+ return this.#group(SearchApi);
3597
+ }
3598
+ get streams() {
3599
+ return this.#group(StreamsApi);
3600
+ }
3601
+ get subscriptions() {
3602
+ return this.#group(SubscriptionsApi);
3603
+ }
3604
+ get tags() {
3605
+ return this.#group(TagsApi);
3606
+ }
3607
+ get teams() {
3608
+ return this.#group(TeamsApi);
3609
+ }
3610
+ get users() {
3611
+ return this.#group(UsersApi);
3612
+ }
3613
+ get videos() {
3614
+ return this.#group(VideosApi);
3615
+ }
3616
+ get whispers() {
3617
+ return this.#group(WhispersApi);
3618
+ }
3619
+ as(userId) {
3620
+ return new _Twitch({ clientId: this.#shared.auth.clientId }, this.#shared, userId);
3621
+ }
3622
+ request(endpoint) {
3623
+ return this.#rest.request(endpoint);
3624
+ }
3625
+ paginate(endpoint, config) {
3626
+ return this.#rest.paginate(endpoint, config);
3627
+ }
3628
+ on(event, listener) {
3629
+ return this.#shared.events.on(event, listener);
3630
+ }
3631
+ once(event, listener) {
3632
+ return this.#shared.events.once(event, listener);
3633
+ }
3634
+ off(event, listener) {
3635
+ this.#shared.events.off(event, listener);
3636
+ }
3637
+ rateLimitOf(userId) {
3638
+ const state = this.#shared.limiter.get(userId ? `user:${userId}` : "app");
3639
+ return state && state.resetAt > Date.now() ? { ...state } : void 0;
3640
+ }
3641
+ close() {
3642
+ this.#shared.auth.close();
3643
+ for (const close of this.#shared.config.closers) close();
3644
+ this.#shared.config.closers.clear();
3645
+ }
3646
+ #group(Group) {
3647
+ let group = this.#groups.get(Group);
3648
+ if (!group) this.#groups.set(Group, group = new Group(this.#rest));
3649
+ return group;
3650
+ }
3651
+ };
3652
+
3653
+ // src/auth/scopes.ts
3654
+ var SCOPES = [
3655
+ "analytics:read:extensions",
3656
+ "analytics:read:games",
3657
+ "bits:read",
3658
+ "channel:bot",
3659
+ "channel:edit:commercial",
3660
+ "channel:manage:ads",
3661
+ "channel:manage:broadcast",
3662
+ "channel:manage:clips",
3663
+ "channel:manage:extensions",
3664
+ "channel:manage:guest_star",
3665
+ "channel:manage:moderators",
3666
+ "channel:manage:polls",
3667
+ "channel:manage:predictions",
3668
+ "channel:manage:raids",
3669
+ "channel:manage:redemptions",
3670
+ "channel:manage:schedule",
3671
+ "channel:manage:videos",
3672
+ "channel:manage:vips",
3673
+ "channel:moderate",
3674
+ "channel:read:ads",
3675
+ "channel:read:charity",
3676
+ "channel:read:editors",
3677
+ "channel:read:goals",
3678
+ "channel:read:guest_star",
3679
+ "channel:read:hype_train",
3680
+ "channel:read:polls",
3681
+ "channel:read:predictions",
3682
+ "channel:read:redemptions",
3683
+ "channel:read:stream_key",
3684
+ "channel:read:subscriptions",
3685
+ "channel:read:vips",
3686
+ "chat:edit",
3687
+ "chat:read",
3688
+ "clips:edit",
3689
+ "editor:manage:clips",
3690
+ "moderation:read",
3691
+ "moderator:manage:announcements",
3692
+ "moderator:manage:automod",
3693
+ "moderator:manage:automod_settings",
3694
+ "moderator:manage:banned_users",
3695
+ "moderator:manage:blocked_terms",
3696
+ "moderator:manage:chat_messages",
3697
+ "moderator:manage:chat_settings",
3698
+ "moderator:manage:guest_star",
3699
+ "moderator:manage:shield_mode",
3700
+ "moderator:manage:shoutouts",
3701
+ "moderator:manage:suspicious_users",
3702
+ "moderator:manage:unban_requests",
3703
+ "moderator:manage:warnings",
3704
+ "moderator:read:automod_settings",
3705
+ "moderator:read:banned_users",
3706
+ "moderator:read:blocked_terms",
3707
+ "moderator:read:chat_messages",
3708
+ "moderator:read:chat_settings",
3709
+ "moderator:read:chatters",
3710
+ "moderator:read:followers",
3711
+ "moderator:read:guest_star",
3712
+ "moderator:read:moderators",
3713
+ "moderator:read:shield_mode",
3714
+ "moderator:read:shoutouts",
3715
+ "moderator:read:suspicious_users",
3716
+ "moderator:read:unban_requests",
3717
+ "moderator:read:vips",
3718
+ "moderator:read:warnings",
3719
+ "user:bot",
3720
+ "user:edit",
3721
+ "user:edit:broadcast",
3722
+ "user:manage:blocked_users",
3723
+ "user:manage:chat_color",
3724
+ "user:manage:whispers",
3725
+ "user:read:blocked_users",
3726
+ "user:read:broadcast",
3727
+ "user:read:chat",
3728
+ "user:read:email",
3729
+ "user:read:emotes",
3730
+ "user:read:follows",
3731
+ "user:read:moderated_channels",
3732
+ "user:read:subscriptions",
3733
+ "user:read:whispers",
3734
+ "user:write:chat",
3735
+ "whispers:read"
3736
+ ];
3737
+
3738
+ export { AdsApi, AnalyticsApi, Auth, BitsApi, ChannelPointsApi, ChannelsApi, CharityApi, ChatApi, ClipsApi, ConduitsApi, ContentLabelsApi, EVENTSUB_VERSIONS, EntitlementsApi, EventSubApi, EventSubListener, EventSubWebSocket, EventSubWebhook, ExtensionsApi, GamesApi, GoalsApi, GuestStarApi, HypeTrainApi, MemoryTokenStore, ModerationApi, Page, Paginator, PollsApi, PredictionsApi, RaidsApi, SCOPES, ScheduleApi, SearchApi, StreamsApi, SubscriptionsApi, TagsApi, TeamsApi, Twitch, TwitchApiError, TwitchAuthError, TwitchError, TwitchEventSubError, TwitchNetworkError, TwitchRateLimitError, TwitchScopeError, TwitchTimeoutError, UsersApi, VideosApi, WhispersApi, activityVersions, channelVersions, chatVersions, resolveEventSubType, verifyEventSubSignature };