tentacle-sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/dist/index.cjs +889 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +1562 -0
  6. package/dist/index.d.ts +1562 -0
  7. package/dist/index.js +884 -0
  8. package/dist/index.js.map +1 -0
  9. package/docs/api/classes/DeviceAuthFlow.md +107 -0
  10. package/docs/api/classes/FileCredentialStore.md +95 -0
  11. package/docs/api/classes/TentacleClient.md +118 -0
  12. package/docs/api/classes/TentacleError.md +69 -0
  13. package/docs/api/index.md +117 -0
  14. package/docs/api/interfaces/AppJson.md +20 -0
  15. package/docs/api/interfaces/ChatMessageEmoji.md +16 -0
  16. package/docs/api/interfaces/ChatMessageEmote.md +16 -0
  17. package/docs/api/interfaces/ChatMessagePosition.md +14 -0
  18. package/docs/api/interfaces/CommonChatMessage.md +27 -0
  19. package/docs/api/interfaces/CommonEventJson.md +32 -0
  20. package/docs/api/interfaces/CredentialStore.md +55 -0
  21. package/docs/api/interfaces/DeviceAuthInitResult.md +17 -0
  22. package/docs/api/interfaces/DeviceAuthOptions.md +18 -0
  23. package/docs/api/interfaces/DeviceAuthPollResult.md +14 -0
  24. package/docs/api/interfaces/DonationUnlock.md +16 -0
  25. package/docs/api/interfaces/FileCredentialStoreOptions.md +13 -0
  26. package/docs/api/interfaces/GiftedSubUnlock.md +15 -0
  27. package/docs/api/interfaces/KickBadge.md +15 -0
  28. package/docs/api/interfaces/KickChatMessageJson.md +35 -0
  29. package/docs/api/interfaces/KickEventJson_ChannelFollow.md +25 -0
  30. package/docs/api/interfaces/KickEventJson_ChannelSubscriptionGifts.md +25 -0
  31. package/docs/api/interfaces/KickEventJson_ChannelSubscriptionNew.md +25 -0
  32. package/docs/api/interfaces/KickEventJson_ChannelSubscriptionRenewal.md +25 -0
  33. package/docs/api/interfaces/KickEventJson_LivestreamStatusUpdated.md +24 -0
  34. package/docs/api/interfaces/RealtimeEvent_StreamChatMessage.md +14 -0
  35. package/docs/api/interfaces/RealtimeEvent_StreamEvent.md +14 -0
  36. package/docs/api/interfaces/RealtimeEvent_StreamViewerActivity.md +14 -0
  37. package/docs/api/interfaces/RealtimeSubscribeOptions.md +16 -0
  38. package/docs/api/interfaces/StreamViewerActivityJson.md +16 -0
  39. package/docs/api/interfaces/SubUnlock.md +15 -0
  40. package/docs/api/interfaces/SubathonStats_Donations.md +23 -0
  41. package/docs/api/interfaces/SubathonStats_GiftedSubscriptions.md +22 -0
  42. package/docs/api/interfaces/SubathonStats_Subscriptions.md +20 -0
  43. package/docs/api/interfaces/TentacleClientConfig.md +14 -0
  44. package/docs/api/interfaces/TentacleClientCreateOptions.md +15 -0
  45. package/docs/api/interfaces/TwitchChatMessageJson.md +41 -0
  46. package/docs/api/interfaces/TwitchEventJson_Cheer.md +29 -0
  47. package/docs/api/interfaces/TwitchEventJson_Follow.md +27 -0
  48. package/docs/api/interfaces/TwitchEventJson_Raid.md +27 -0
  49. package/docs/api/interfaces/TwitchEventJson_RedemptionAdd.md +34 -0
  50. package/docs/api/interfaces/TwitchEventJson_StreamOnline.md +26 -0
  51. package/docs/api/interfaces/TwitchEventJson_Subscription.md +28 -0
  52. package/docs/api/interfaces/TwitchEventJson_SubscriptionGift.md +30 -0
  53. package/docs/api/interfaces/TwitchUserInfo.md +24 -0
  54. package/docs/api/interfaces/ViewerActionJson.md +18 -0
  55. package/docs/api/interfaces/ViewerJson.md +23 -0
  56. package/docs/api/interfaces/ViewerKickJson.md +25 -0
  57. package/docs/api/interfaces/ViewerMiniJson.md +21 -0
  58. package/docs/api/interfaces/ViewerPropertyJsonBase.md +22 -0
  59. package/docs/api/interfaces/ViewerPropertyJson_Bool.md +22 -0
  60. package/docs/api/interfaces/ViewerPropertyJson_Number.md +22 -0
  61. package/docs/api/interfaces/ViewerPropertyJson_String.md +22 -0
  62. package/docs/api/interfaces/ViewerTwitchJson.md +24 -0
  63. package/docs/api/interfaces/ViewersByPropertyOutput.md +17 -0
  64. package/docs/api/type-aliases/DateIsoString.md +9 -0
  65. package/docs/api/type-aliases/KickEventJson.md +9 -0
  66. package/docs/api/type-aliases/OrderDirection.md +9 -0
  67. package/docs/api/type-aliases/RealtimeEvent.md +36 -0
  68. package/docs/api/type-aliases/StreamChatMessageJson.md +9 -0
  69. package/docs/api/type-aliases/StreamEventJson.md +9 -0
  70. package/docs/api/type-aliases/StreamPlatform.md +9 -0
  71. package/docs/api/type-aliases/TwitchEventJson.md +9 -0
  72. package/docs/api/type-aliases/TwitchEventType.md +9 -0
  73. package/docs/api/type-aliases/UnsubscribeFunction.md +22 -0
  74. package/docs/api/type-aliases/ViewerPropertyJson.md +9 -0
  75. package/docs/api/type-aliases/ViewerPropertyType.md +9 -0
  76. package/docs/overview.md +160 -0
  77. package/package.json +54 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,889 @@
1
+ 'use strict';
2
+
3
+ // src/http.ts
4
+ var TentacleError = class extends Error {
5
+ constructor(message, status, code) {
6
+ super(message);
7
+ this.name = "TentacleError";
8
+ this.status = status;
9
+ this.code = code;
10
+ }
11
+ };
12
+ var HttpClient = class {
13
+ constructor(config) {
14
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
15
+ this.accessToken = config.accessToken;
16
+ }
17
+ /**
18
+ * Get the headers for a request.
19
+ */
20
+ getHeaders() {
21
+ const headers = new Headers();
22
+ headers.set("Content-Type", "application/json");
23
+ if (this.accessToken) {
24
+ headers.set("Authorization", `Bearer ${this.accessToken}`);
25
+ }
26
+ return headers;
27
+ }
28
+ /**
29
+ * Build the tRPC URL for a procedure call.
30
+ *
31
+ * @param procedure - Full procedure path (e.g., "apps.list")
32
+ * @param input - Optional input for queries (will be URL encoded)
33
+ */
34
+ buildTrpcUrl(procedure, input) {
35
+ const url = new URL(`${this.baseUrl}/api/trpc/${procedure}`);
36
+ if (input !== void 0) {
37
+ url.searchParams.set("input", JSON.stringify(input));
38
+ }
39
+ return url.toString();
40
+ }
41
+ /**
42
+ * Handle the tRPC response.
43
+ */
44
+ async handleResponse(response) {
45
+ const data = await response.json();
46
+ if (data.error) {
47
+ const message = data.error.message || `Request failed with status ${response.status}`;
48
+ const code = data.error.data?.code;
49
+ const status = data.error.data?.httpStatus || response.status;
50
+ throw new TentacleError(message, status, code);
51
+ }
52
+ if (!response.ok) {
53
+ throw new TentacleError(`Request failed with status ${response.status}`, response.status);
54
+ }
55
+ if (data.result?.data === void 0) {
56
+ throw new TentacleError("Invalid response: missing result data", response.status);
57
+ }
58
+ return data.result.data;
59
+ }
60
+ /**
61
+ * Make a tRPC query (GET request).
62
+ *
63
+ * @param procedure - Full procedure path (e.g., "apps.list")
64
+ * @param input - Optional input data
65
+ * @returns The response data
66
+ * @throws {TentacleError} If the request fails
67
+ */
68
+ async query(procedure, input) {
69
+ const url = this.buildTrpcUrl(procedure, input);
70
+ const response = await fetch(url, {
71
+ method: "GET",
72
+ headers: this.getHeaders()
73
+ });
74
+ return this.handleResponse(response);
75
+ }
76
+ /**
77
+ * Make a tRPC mutation (POST request).
78
+ *
79
+ * @param procedure - Full procedure path (e.g., "apps.create")
80
+ * @param input - Input data for the mutation
81
+ * @returns The response data
82
+ * @throws {TentacleError} If the request fails
83
+ */
84
+ async mutate(procedure, input) {
85
+ const url = this.buildTrpcUrl(procedure);
86
+ const response = await fetch(url, {
87
+ method: "POST",
88
+ headers: this.getHeaders(),
89
+ body: JSON.stringify(input)
90
+ });
91
+ return this.handleResponse(response);
92
+ }
93
+ /**
94
+ * Get the base URL for SSE connections.
95
+ */
96
+ getBaseUrl() {
97
+ return this.baseUrl;
98
+ }
99
+ /**
100
+ * Get the access token for SSE connections.
101
+ */
102
+ getAccessToken() {
103
+ return this.accessToken;
104
+ }
105
+ };
106
+
107
+ // src/realtime.ts
108
+ function createRealtimeSubscription(baseUrl, accessToken, options) {
109
+ const url = `${baseUrl}/api/trpc/realtime.subscribe`;
110
+ const controller = new AbortController();
111
+ const connect = async () => {
112
+ try {
113
+ const response = await fetch(url, {
114
+ method: "GET",
115
+ headers: {
116
+ Accept: "text/event-stream",
117
+ Authorization: `Bearer ${accessToken}`,
118
+ "Cache-Control": "no-cache"
119
+ },
120
+ signal: controller.signal
121
+ });
122
+ if (!response.ok) {
123
+ throw new Error(`Failed to connect: ${response.status} ${response.statusText}`);
124
+ }
125
+ if (!response.body) {
126
+ throw new Error("Response body is null");
127
+ }
128
+ options.onOpen?.();
129
+ const reader = response.body.getReader();
130
+ const decoder = new TextDecoder();
131
+ let buffer = "";
132
+ while (true) {
133
+ const { done, value } = await reader.read();
134
+ if (done) {
135
+ options.onClose?.();
136
+ break;
137
+ }
138
+ buffer += decoder.decode(value, { stream: true });
139
+ const lines = buffer.split("\n");
140
+ buffer = lines.pop() ?? "";
141
+ let eventData = "";
142
+ for (const line of lines) {
143
+ if (line.startsWith("data: ")) {
144
+ eventData += line.slice(6);
145
+ } else if (line === "" && eventData) {
146
+ try {
147
+ const parsed = JSON.parse(eventData);
148
+ if (parsed.error) {
149
+ options.onError?.(new Error(parsed.error.message));
150
+ } else if (parsed.result?.data) {
151
+ if (parsed.result.data.kind !== "ConnectionConfirmed") {
152
+ options.onEvent(parsed.result.data);
153
+ }
154
+ }
155
+ } catch (parseError) {
156
+ console.error("Failed to parse SSE event:", parseError);
157
+ }
158
+ eventData = "";
159
+ }
160
+ }
161
+ }
162
+ } catch (error) {
163
+ if (error instanceof Error && error.name === "AbortError") {
164
+ options.onClose?.();
165
+ return;
166
+ }
167
+ options.onError?.(error instanceof Error ? error : new Error(String(error)));
168
+ if (!controller.signal.aborted) {
169
+ setTimeout(() => {
170
+ if (!controller.signal.aborted) {
171
+ void connect();
172
+ }
173
+ }, 5e3);
174
+ }
175
+ }
176
+ };
177
+ void connect();
178
+ return () => {
179
+ controller.abort();
180
+ };
181
+ }
182
+ var RealtimeApi = class {
183
+ constructor(baseUrl, accessToken) {
184
+ this.baseUrl = baseUrl;
185
+ this.accessToken = accessToken;
186
+ }
187
+ /**
188
+ * Subscribe to realtime events from Twitch and Kick.
189
+ *
190
+ * This establishes a Server-Sent Events (SSE) connection to receive live updates:
191
+ * - **StreamChatMessage**: Chat messages from Twitch or Kick
192
+ * - **StreamEvent**: Events like follows, subscriptions, raids, cheers
193
+ * - **StreamViewerActivity**: Viewer activity updates
194
+ *
195
+ * Each event includes viewer data (when available) via the `$viewerId` field
196
+ * and platform-specific user info.
197
+ *
198
+ * @param options - Subscription options with event callbacks
199
+ * @returns Function to unsubscribe and close the connection
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * const unsubscribe = client.realtime.subscribe({
204
+ * onEvent: (event) => {
205
+ * switch (event.kind) {
206
+ * case 'StreamChatMessage':
207
+ * // Chat message from Twitch or Kick
208
+ * const msg = event.payload
209
+ * console.log(`[${msg.$platform}] ${msg.$text}`)
210
+ * console.log(`Viewer ID: ${msg.$viewerId}`)
211
+ * break
212
+ *
213
+ * case 'StreamEvent':
214
+ * // Stream event (follow, sub, raid, etc.)
215
+ * const evt = event.payload
216
+ * if (evt.$platform === 'twitch') {
217
+ * switch (evt.$type) {
218
+ * case 'channel.follow':
219
+ * console.log(`New Twitch follower: ${evt.userDisplayName}`)
220
+ * break
221
+ * case 'channel.subscribe':
222
+ * console.log(`New Twitch sub: ${evt.userDisplayName}`)
223
+ * break
224
+ * case 'channel.raid':
225
+ * console.log(`Raid from ${evt.fromBroadcasterUserName} with ${evt.viewers} viewers`)
226
+ * break
227
+ * }
228
+ * } else if (evt.$platform === 'kick') {
229
+ * switch (evt.$type) {
230
+ * case 'channel.followed':
231
+ * console.log(`New Kick follower: ${evt.username}`)
232
+ * break
233
+ * case 'channel.subscription.new':
234
+ * console.log(`New Kick sub: ${evt.username}`)
235
+ * break
236
+ * }
237
+ * }
238
+ * break
239
+ *
240
+ * case 'StreamViewerActivity':
241
+ * console.log(`Viewer activity: ${event.payload.viewerId}`)
242
+ * break
243
+ * }
244
+ * },
245
+ * onError: (error) => console.error('Connection error:', error),
246
+ * onOpen: () => console.log('Connected!'),
247
+ * onClose: () => console.log('Disconnected'),
248
+ * })
249
+ *
250
+ * // When done, unsubscribe:
251
+ * unsubscribe()
252
+ * ```
253
+ *
254
+ * @throws {Error} If no access token is configured
255
+ */
256
+ subscribe(options) {
257
+ if (!this.accessToken) {
258
+ throw new Error("Access token is required for realtime subscription");
259
+ }
260
+ return createRealtimeSubscription(this.baseUrl, this.accessToken, options);
261
+ }
262
+ };
263
+
264
+ // src/auth/device-auth.ts
265
+ var DeviceAuthFlow = class {
266
+ constructor(baseUrl) {
267
+ this.baseUrl = baseUrl.replace(/\/$/, "");
268
+ }
269
+ /**
270
+ * Initiates a device authorization request.
271
+ *
272
+ * @param options - Options for the request.
273
+ * @returns Device and user codes for authorization.
274
+ */
275
+ async initiate(options = {}) {
276
+ const url = `${this.baseUrl}/api/trpc/access.initiateDeviceAuth`;
277
+ const response = await fetch(url, {
278
+ method: "POST",
279
+ headers: { "Content-Type": "application/json" },
280
+ body: JSON.stringify({ clientInfo: options.clientInfo })
281
+ });
282
+ if (!response.ok) {
283
+ throw new Error(`Failed to initiate device auth: ${response.statusText}`);
284
+ }
285
+ const data = await response.json();
286
+ if (data.error) {
287
+ throw new Error(`Device auth error: ${data.error.message}`);
288
+ }
289
+ if (!data.result?.data) {
290
+ throw new Error("Invalid response from device auth endpoint");
291
+ }
292
+ return data.result.data;
293
+ }
294
+ /**
295
+ * Polls for the current authorization status.
296
+ *
297
+ * @param deviceCode - The device code from initiate().
298
+ * @returns Current authorization status.
299
+ */
300
+ async poll(deviceCode) {
301
+ const url = new URL(`${this.baseUrl}/api/trpc/access.pollDeviceAuth`);
302
+ url.searchParams.set("input", JSON.stringify({ deviceCode }));
303
+ const response = await fetch(url.toString(), {
304
+ method: "GET",
305
+ headers: { "Content-Type": "application/json" }
306
+ });
307
+ if (!response.ok) {
308
+ throw new Error(`Failed to poll device auth: ${response.statusText}`);
309
+ }
310
+ const data = await response.json();
311
+ if (data.error) {
312
+ throw new Error(`Device auth poll error: ${data.error.message}`);
313
+ }
314
+ if (!data.result?.data) {
315
+ throw new Error("Invalid response from poll endpoint");
316
+ }
317
+ return data.result.data;
318
+ }
319
+ /**
320
+ * Polls until authorization is complete (approved, denied, or expired).
321
+ *
322
+ * @param deviceCode - The device code from initiate().
323
+ * @param intervalMs - Polling interval in milliseconds.
324
+ * @param timeoutMs - Maximum time to wait in milliseconds.
325
+ * @returns Final authorization result.
326
+ */
327
+ async pollUntilComplete(deviceCode, intervalMs = 5e3, timeoutMs = 6e5) {
328
+ const startTime = Date.now();
329
+ while (Date.now() - startTime < timeoutMs) {
330
+ const result = await this.poll(deviceCode);
331
+ if (result.status !== "pending") {
332
+ return result;
333
+ }
334
+ await new Promise((resolve) => {
335
+ setTimeout(resolve, intervalMs);
336
+ });
337
+ }
338
+ return { status: "expired" };
339
+ }
340
+ };
341
+ async function validateAccessToken(baseUrl, token) {
342
+ const url = new URL(`${baseUrl.replace(/\/$/, "")}/api/trpc/access.getAccessTokenStatus`);
343
+ url.searchParams.set("input", JSON.stringify({ jwt: token }));
344
+ try {
345
+ const response = await fetch(url.toString(), {
346
+ method: "GET",
347
+ headers: {
348
+ "Content-Type": "application/json",
349
+ Authorization: `Bearer ${token}`
350
+ }
351
+ });
352
+ if (!response.ok) {
353
+ return false;
354
+ }
355
+ const data = await response.json();
356
+ return data.result?.data.isValid ?? false;
357
+ } catch {
358
+ return false;
359
+ }
360
+ }
361
+ async function performDeviceAuth(options) {
362
+ const { baseUrl, credentialStore, deviceAuthOptions } = options;
363
+ const cachedToken = await credentialStore.getAccessToken();
364
+ if (cachedToken) {
365
+ const isValid = await validateAccessToken(baseUrl, cachedToken);
366
+ if (isValid) {
367
+ return cachedToken;
368
+ }
369
+ await credentialStore.clearAccessToken();
370
+ }
371
+ const flow = new DeviceAuthFlow(baseUrl);
372
+ const { deviceCode, userCode, verificationUrl, interval } = await flow.initiate({
373
+ clientInfo: deviceAuthOptions.clientInfo
374
+ });
375
+ deviceAuthOptions.onAuthRequired({ verificationUrl, userCode });
376
+ const timeoutMs = deviceAuthOptions.pollTimeout ?? 6e5;
377
+ const result = await flow.pollUntilComplete(deviceCode, interval * 1e3, timeoutMs);
378
+ if (result.status === "approved" && result.accessToken) {
379
+ await credentialStore.setAccessToken(result.accessToken);
380
+ deviceAuthOptions.onAuthApproved?.();
381
+ return result.accessToken;
382
+ }
383
+ if (result.status === "denied") {
384
+ deviceAuthOptions.onAuthDenied?.();
385
+ throw new Error("Device authorization was denied");
386
+ }
387
+ if (result.status === "expired") {
388
+ deviceAuthOptions.onAuthTimeout?.();
389
+ throw new Error("Device authorization timed out");
390
+ }
391
+ throw new Error(`Unexpected authorization status: ${result.status}`);
392
+ }
393
+
394
+ // src/client.ts
395
+ var AppsApi = class {
396
+ constructor(http) {
397
+ this.http = http;
398
+ }
399
+ /**
400
+ * Create a new app.
401
+ *
402
+ * @param options - App creation options
403
+ * @param options.name - Name of the app
404
+ * @returns The created app
405
+ *
406
+ * @example
407
+ * ```typescript
408
+ * const { app } = await client.apps.create({ name: "My Game" })
409
+ * console.log(`Created app: ${app.id}`)
410
+ * ```
411
+ */
412
+ async create(options) {
413
+ return this.http.mutate("apps.create", options);
414
+ }
415
+ /**
416
+ * List all apps for the current user.
417
+ *
418
+ * @returns Array of apps
419
+ *
420
+ * @example
421
+ * ```typescript
422
+ * const { apps } = await client.apps.list()
423
+ * for (const app of apps) {
424
+ * console.log(`${app.name}: ${app.id}`)
425
+ * }
426
+ * ```
427
+ */
428
+ async list() {
429
+ return this.http.query("apps.list");
430
+ }
431
+ /**
432
+ * Get an app by name.
433
+ *
434
+ * @param options - Query options
435
+ * @param options.name - Name of the app
436
+ * @returns The app
437
+ *
438
+ * @example
439
+ * ```typescript
440
+ * const { app } = await client.apps.getByName({ name: "My Game" })
441
+ * console.log(`App ID: ${app.id}`)
442
+ * ```
443
+ */
444
+ async getByName(options) {
445
+ return this.http.query("apps.getByName", options);
446
+ }
447
+ /**
448
+ * List apps for a viewer.
449
+ * Requires viewer authentication.
450
+ *
451
+ * @param options - Query options
452
+ * @param options.where - Filter options
453
+ * @param options.where.isActive - Filter by active status
454
+ * @returns Array of apps
455
+ *
456
+ * @example
457
+ * ```typescript
458
+ * const { apps } = await client.apps.listForViewer({ where: { isActive: true } })
459
+ * ```
460
+ */
461
+ async listForViewer(options) {
462
+ return this.http.query("apps.listForViewer", options);
463
+ }
464
+ /**
465
+ * Create or update viewer properties.
466
+ *
467
+ * @param properties - Array of viewer properties to create/update
468
+ * @returns Success status
469
+ *
470
+ * @example
471
+ * ```typescript
472
+ * await client.apps.createViewerProperties([
473
+ * { id: "prop1", appId: "app1", viewerId: "v1", name: "score", type: "number", value: 100 },
474
+ * ])
475
+ * ```
476
+ */
477
+ async createViewerProperties(properties) {
478
+ return this.http.mutate("apps.createViewerProperties", properties);
479
+ }
480
+ /**
481
+ * Update viewer properties.
482
+ *
483
+ * @param properties - Array of viewer properties to update
484
+ * @returns Success status
485
+ *
486
+ * @example
487
+ * ```typescript
488
+ * await client.apps.updateViewerProperties([
489
+ * { id: "prop1", appId: "app1", viewerId: "v1", name: "score", type: "number", value: 150 },
490
+ * ])
491
+ * ```
492
+ */
493
+ async updateViewerProperties(properties) {
494
+ return this.http.mutate("apps.updateViewerProperties", properties);
495
+ }
496
+ /**
497
+ * Get viewers by property.
498
+ *
499
+ * @param options - Query options
500
+ * @param options.appId - App ID
501
+ * @param options.propertyName - Property name to query
502
+ * @param options.propertyType - Property type
503
+ * @param options.orderDirection - Order direction (asc/desc)
504
+ * @param options.take - Number of results (1-1000, default 20)
505
+ * @returns Viewers with the specified property
506
+ *
507
+ * @example
508
+ * ```typescript
509
+ * const result = await client.apps.getViewersByProperty({
510
+ * appId: "app1",
511
+ * propertyName: "score",
512
+ * propertyType: "number",
513
+ * orderDirection: "desc",
514
+ * take: 10,
515
+ * })
516
+ * for (const viewer of result.viewers) {
517
+ * console.log(`${viewer.twitch?.displayName}: ${viewer.properties.score.value}`)
518
+ * }
519
+ * ```
520
+ */
521
+ async getViewersByProperty(options) {
522
+ return this.http.query("apps.getViewersByProperty", options);
523
+ }
524
+ };
525
+ var StreamApi = class {
526
+ constructor(http) {
527
+ this.http = http;
528
+ }
529
+ /**
530
+ * Get the latest chat messages from Twitch and Kick.
531
+ *
532
+ * @param options - Query options
533
+ * @param options.take - Maximum number of messages to return (default: 50)
534
+ * @param options.isCommand - Filter to only command messages (starting with !)
535
+ * @returns Array of chat messages from both platforms
536
+ *
537
+ * @example
538
+ * ```typescript
539
+ * // Get latest 50 chat messages
540
+ * const messages = await client.stream.getChatMessages()
541
+ *
542
+ * // Get latest 10 command messages
543
+ * const commands = await client.stream.getChatMessages({
544
+ * take: 10,
545
+ * isCommand: true,
546
+ * })
547
+ *
548
+ * for (const msg of messages) {
549
+ * console.log(`[${msg.$platform}] ${msg.$text}`)
550
+ * }
551
+ * ```
552
+ */
553
+ async getChatMessages(options) {
554
+ return this.http.query("stream.getLatestChatMessages", options);
555
+ }
556
+ /**
557
+ * Get the latest stream events from Twitch and Kick.
558
+ *
559
+ * Events include follows, subscriptions, raids, cheers, and more.
560
+ *
561
+ * @param options - Query options
562
+ * @param options.take - Maximum number of events to return (default: 50)
563
+ * @returns Array of stream events from both platforms
564
+ *
565
+ * @example
566
+ * ```typescript
567
+ * const events = await client.stream.getEvents({ take: 20 })
568
+ *
569
+ * for (const event of events) {
570
+ * if (event.$platform === 'twitch') {
571
+ * switch (event.$type) {
572
+ * case 'channel.follow':
573
+ * console.log(`New follower: ${event.userDisplayName}`)
574
+ * break
575
+ * case 'channel.subscribe':
576
+ * console.log(`New subscriber: ${event.userDisplayName}`)
577
+ * break
578
+ * }
579
+ * }
580
+ * }
581
+ * ```
582
+ */
583
+ async getEvents(options) {
584
+ return this.http.query("stream.getLatestEvents", options);
585
+ }
586
+ };
587
+ var SubathonApi = class {
588
+ constructor(http) {
589
+ this.http = http;
590
+ }
591
+ /**
592
+ * Get subscription stats.
593
+ *
594
+ * @returns Subscription statistics
595
+ *
596
+ * @example
597
+ * ```typescript
598
+ * const stats = await client.subathonStats.getSubs()
599
+ * console.log(`Subs: ${stats.count}/${stats.goal}`)
600
+ * ```
601
+ */
602
+ async getSubs() {
603
+ return this.http.query("subathonStats.subs");
604
+ }
605
+ /**
606
+ * Get gifted subscription stats.
607
+ *
608
+ * @returns Gifted subscription statistics
609
+ *
610
+ * @example
611
+ * ```typescript
612
+ * const stats = await client.subathonStats.getGiftedSubs()
613
+ * console.log(`Gifted subs: ${stats.count}`)
614
+ * ```
615
+ */
616
+ async getGiftedSubs() {
617
+ return this.http.query("subathonStats.giftedSubs");
618
+ }
619
+ /**
620
+ * Get donation stats.
621
+ *
622
+ * @returns Donation statistics
623
+ *
624
+ * @example
625
+ * ```typescript
626
+ * const stats = await client.subathonStats.getDonations()
627
+ * console.log(`Donations: $${stats.dollars}`)
628
+ * ```
629
+ */
630
+ async getDonations() {
631
+ return this.http.query("subathonStats.donations");
632
+ }
633
+ /**
634
+ * Get subscription stats for overlay.
635
+ * Public endpoint - no authentication required.
636
+ *
637
+ * @param options - Query options
638
+ * @param options.email - User email
639
+ * @returns Subscription statistics or null if user not found
640
+ *
641
+ * @example
642
+ * ```typescript
643
+ * const stats = await client.subathonStats.getOverlaySubs({ email: "user@example.com" })
644
+ * if (stats) {
645
+ * console.log(`Subs: ${stats.count}`)
646
+ * }
647
+ * ```
648
+ */
649
+ async getOverlaySubs(options) {
650
+ return this.http.query("subathonStats.overlaySubs", options);
651
+ }
652
+ /**
653
+ * Get gifted subscription stats for overlay.
654
+ * Public endpoint - no authentication required.
655
+ *
656
+ * @param options - Query options
657
+ * @param options.email - User email
658
+ * @returns Gifted subscription statistics or null if user not found
659
+ *
660
+ * @example
661
+ * ```typescript
662
+ * const stats = await client.subathonStats.getOverlayGiftedSubs({ email: "user@example.com" })
663
+ * ```
664
+ */
665
+ async getOverlayGiftedSubs(options) {
666
+ return this.http.query("subathonStats.overlayGiftedSubs", options);
667
+ }
668
+ /**
669
+ * Get donation stats for overlay.
670
+ * Public endpoint - no authentication required.
671
+ *
672
+ * @param options - Query options
673
+ * @param options.email - User email
674
+ * @returns Donation statistics or null if user not found
675
+ *
676
+ * @example
677
+ * ```typescript
678
+ * const stats = await client.subathonStats.getOverlayDonations({ email: "user@example.com" })
679
+ * ```
680
+ */
681
+ async getOverlayDonations(options) {
682
+ return this.http.query("subathonStats.overlayDonations", options);
683
+ }
684
+ };
685
+ var ViewerApi = class {
686
+ constructor(http) {
687
+ this.http = http;
688
+ }
689
+ /**
690
+ * Create a viewer action.
691
+ * Requires viewer authentication.
692
+ *
693
+ * @param options - Action options
694
+ * @param options.name - Action name
695
+ * @param options.data - Action data
696
+ * @param options.appId - App ID
697
+ * @returns Success status
698
+ *
699
+ * @example
700
+ * ```typescript
701
+ * await client.viewer.createAction({
702
+ * name: "button_click",
703
+ * data: { buttonId: "play" },
704
+ * appId: "app1",
705
+ * })
706
+ * ```
707
+ */
708
+ async createAction(options) {
709
+ return this.http.mutate("viewer.createViewerAction", options);
710
+ }
711
+ /**
712
+ * Get mini viewer data.
713
+ *
714
+ * @param options - Query options
715
+ * @param options.viewerId - Viewer ID
716
+ * @returns Viewer mini data
717
+ *
718
+ * @example
719
+ * ```typescript
720
+ * const viewer = await client.viewer.getMini({ viewerId: "v1" })
721
+ * console.log(`Twitch: ${viewer.twitch?.displayName}`)
722
+ * ```
723
+ */
724
+ async getMini(options) {
725
+ return this.http.query("viewer.getViewerMini", options);
726
+ }
727
+ /**
728
+ * Get full viewer data including properties.
729
+ *
730
+ * @param options - Query options
731
+ * @param options.viewerId - Viewer ID
732
+ * @returns Full viewer data
733
+ *
734
+ * @example
735
+ * ```typescript
736
+ * const viewer = await client.viewer.get({ viewerId: "v1" })
737
+ * console.log(`Twitch: ${viewer.twitch?.displayName}`)
738
+ * if (viewer.properties) {
739
+ * for (const [name, prop] of Object.entries(viewer.properties)) {
740
+ * console.log(`${name}: ${prop.value}`)
741
+ * }
742
+ * }
743
+ * ```
744
+ */
745
+ async get(options) {
746
+ return this.http.query("viewer.getViewerFull", options);
747
+ }
748
+ };
749
+ var TentacleClient = class _TentacleClient {
750
+ /**
751
+ * Creates a TentacleClient with automatic authentication.
752
+ *
753
+ * This factory method handles the device authorization flow:
754
+ * 1. Checks for a cached token in the credential store
755
+ * 2. Validates the cached token with the API
756
+ * 3. If invalid or missing, initiates device authorization
757
+ * 4. Caches the new token after successful authorization
758
+ *
759
+ * @param options - Creation options
760
+ * @returns A configured TentacleClient
761
+ *
762
+ * @example
763
+ * ```typescript
764
+ * const client = await TentacleClient.create({
765
+ * baseUrl: "https://api.tentacle.live",
766
+ * credentialStore: new FileCredentialStore(),
767
+ * deviceAuthOptions: {
768
+ * clientInfo: "My Unreal Game",
769
+ * onAuthRequired: ({ verificationUrl, userCode }) => {
770
+ * console.log(`Visit: ${verificationUrl}`)
771
+ * console.log(`Enter code: ${userCode}`)
772
+ * },
773
+ * },
774
+ * })
775
+ * ```
776
+ */
777
+ static async create(options) {
778
+ const accessToken = await performDeviceAuth({
779
+ baseUrl: options.baseUrl,
780
+ credentialStore: options.credentialStore,
781
+ deviceAuthOptions: options.deviceAuthOptions
782
+ });
783
+ return new _TentacleClient({
784
+ baseUrl: options.baseUrl,
785
+ accessToken
786
+ });
787
+ }
788
+ /**
789
+ * Create a new Tentacle client.
790
+ *
791
+ * @param config - Client configuration
792
+ * @param config.baseUrl - Base URL of the Tentacle API
793
+ * @param config.accessToken - Access token for authentication
794
+ *
795
+ * @example
796
+ * ```typescript
797
+ * const client = new TentacleClient({
798
+ * baseUrl: 'https://api.tentacle.live',
799
+ * accessToken: 'your-access-token',
800
+ * })
801
+ * ```
802
+ */
803
+ constructor(config) {
804
+ this.http = new HttpClient(config);
805
+ this.apps = new AppsApi(this.http);
806
+ this.realtime = new RealtimeApi(config.baseUrl, config.accessToken);
807
+ this.stream = new StreamApi(this.http);
808
+ this.subathon = new SubathonApi(this.http);
809
+ this.viewer = new ViewerApi(this.http);
810
+ }
811
+ };
812
+
813
+ // src/auth/file-credential-store.ts
814
+ var FileCredentialStore = class {
815
+ constructor(options = {}) {
816
+ this.path = options.path ?? this.getDefaultPath();
817
+ }
818
+ /**
819
+ * Gets the default credentials file path.
820
+ */
821
+ getDefaultPath() {
822
+ const home = process.env.HOME || process.env.USERPROFILE || "";
823
+ return `${home}/.tentacle/credentials.json`;
824
+ }
825
+ /**
826
+ * Retrieves the stored access token.
827
+ */
828
+ async getAccessToken() {
829
+ try {
830
+ const fs = await import('fs/promises');
831
+ const content = await fs.readFile(this.path, "utf-8");
832
+ const data = JSON.parse(content);
833
+ return data.accessToken ?? null;
834
+ } catch {
835
+ return null;
836
+ }
837
+ }
838
+ /**
839
+ * Stores an access token.
840
+ *
841
+ * Creates the directory if it doesn't exist.
842
+ * Sets file permissions to 0o600 (user read/write only).
843
+ */
844
+ async setAccessToken(token) {
845
+ const fs = await import('fs/promises');
846
+ const path = await import('path');
847
+ const dir = path.dirname(this.path);
848
+ await fs.mkdir(dir, { recursive: true, mode: 448 });
849
+ let existingData = {};
850
+ try {
851
+ const content = await fs.readFile(this.path, "utf-8");
852
+ existingData = JSON.parse(content);
853
+ } catch {
854
+ }
855
+ const data = {
856
+ ...existingData,
857
+ accessToken: token
858
+ };
859
+ await fs.writeFile(this.path, JSON.stringify(data, null, 2), {
860
+ mode: 384
861
+ });
862
+ }
863
+ /**
864
+ * Removes the stored access token.
865
+ */
866
+ async clearAccessToken() {
867
+ const fs = await import('fs/promises');
868
+ try {
869
+ const content = await fs.readFile(this.path, "utf-8");
870
+ const data = JSON.parse(content);
871
+ delete data.accessToken;
872
+ if (Object.keys(data).length === 0) {
873
+ await fs.unlink(this.path);
874
+ } else {
875
+ await fs.writeFile(this.path, JSON.stringify(data, null, 2), {
876
+ mode: 384
877
+ });
878
+ }
879
+ } catch {
880
+ }
881
+ }
882
+ };
883
+
884
+ exports.DeviceAuthFlow = DeviceAuthFlow;
885
+ exports.FileCredentialStore = FileCredentialStore;
886
+ exports.TentacleClient = TentacleClient;
887
+ exports.TentacleError = TentacleError;
888
+ //# sourceMappingURL=index.cjs.map
889
+ //# sourceMappingURL=index.cjs.map