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.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/index.cjs +889 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1562 -0
- package/dist/index.d.ts +1562 -0
- package/dist/index.js +884 -0
- package/dist/index.js.map +1 -0
- package/docs/api/classes/DeviceAuthFlow.md +107 -0
- package/docs/api/classes/FileCredentialStore.md +95 -0
- package/docs/api/classes/TentacleClient.md +118 -0
- package/docs/api/classes/TentacleError.md +69 -0
- package/docs/api/index.md +117 -0
- package/docs/api/interfaces/AppJson.md +20 -0
- package/docs/api/interfaces/ChatMessageEmoji.md +16 -0
- package/docs/api/interfaces/ChatMessageEmote.md +16 -0
- package/docs/api/interfaces/ChatMessagePosition.md +14 -0
- package/docs/api/interfaces/CommonChatMessage.md +27 -0
- package/docs/api/interfaces/CommonEventJson.md +32 -0
- package/docs/api/interfaces/CredentialStore.md +55 -0
- package/docs/api/interfaces/DeviceAuthInitResult.md +17 -0
- package/docs/api/interfaces/DeviceAuthOptions.md +18 -0
- package/docs/api/interfaces/DeviceAuthPollResult.md +14 -0
- package/docs/api/interfaces/DonationUnlock.md +16 -0
- package/docs/api/interfaces/FileCredentialStoreOptions.md +13 -0
- package/docs/api/interfaces/GiftedSubUnlock.md +15 -0
- package/docs/api/interfaces/KickBadge.md +15 -0
- package/docs/api/interfaces/KickChatMessageJson.md +35 -0
- package/docs/api/interfaces/KickEventJson_ChannelFollow.md +25 -0
- package/docs/api/interfaces/KickEventJson_ChannelSubscriptionGifts.md +25 -0
- package/docs/api/interfaces/KickEventJson_ChannelSubscriptionNew.md +25 -0
- package/docs/api/interfaces/KickEventJson_ChannelSubscriptionRenewal.md +25 -0
- package/docs/api/interfaces/KickEventJson_LivestreamStatusUpdated.md +24 -0
- package/docs/api/interfaces/RealtimeEvent_StreamChatMessage.md +14 -0
- package/docs/api/interfaces/RealtimeEvent_StreamEvent.md +14 -0
- package/docs/api/interfaces/RealtimeEvent_StreamViewerActivity.md +14 -0
- package/docs/api/interfaces/RealtimeSubscribeOptions.md +16 -0
- package/docs/api/interfaces/StreamViewerActivityJson.md +16 -0
- package/docs/api/interfaces/SubUnlock.md +15 -0
- package/docs/api/interfaces/SubathonStats_Donations.md +23 -0
- package/docs/api/interfaces/SubathonStats_GiftedSubscriptions.md +22 -0
- package/docs/api/interfaces/SubathonStats_Subscriptions.md +20 -0
- package/docs/api/interfaces/TentacleClientConfig.md +14 -0
- package/docs/api/interfaces/TentacleClientCreateOptions.md +15 -0
- package/docs/api/interfaces/TwitchChatMessageJson.md +41 -0
- package/docs/api/interfaces/TwitchEventJson_Cheer.md +29 -0
- package/docs/api/interfaces/TwitchEventJson_Follow.md +27 -0
- package/docs/api/interfaces/TwitchEventJson_Raid.md +27 -0
- package/docs/api/interfaces/TwitchEventJson_RedemptionAdd.md +34 -0
- package/docs/api/interfaces/TwitchEventJson_StreamOnline.md +26 -0
- package/docs/api/interfaces/TwitchEventJson_Subscription.md +28 -0
- package/docs/api/interfaces/TwitchEventJson_SubscriptionGift.md +30 -0
- package/docs/api/interfaces/TwitchUserInfo.md +24 -0
- package/docs/api/interfaces/ViewerActionJson.md +18 -0
- package/docs/api/interfaces/ViewerJson.md +23 -0
- package/docs/api/interfaces/ViewerKickJson.md +25 -0
- package/docs/api/interfaces/ViewerMiniJson.md +21 -0
- package/docs/api/interfaces/ViewerPropertyJsonBase.md +22 -0
- package/docs/api/interfaces/ViewerPropertyJson_Bool.md +22 -0
- package/docs/api/interfaces/ViewerPropertyJson_Number.md +22 -0
- package/docs/api/interfaces/ViewerPropertyJson_String.md +22 -0
- package/docs/api/interfaces/ViewerTwitchJson.md +24 -0
- package/docs/api/interfaces/ViewersByPropertyOutput.md +17 -0
- package/docs/api/type-aliases/DateIsoString.md +9 -0
- package/docs/api/type-aliases/KickEventJson.md +9 -0
- package/docs/api/type-aliases/OrderDirection.md +9 -0
- package/docs/api/type-aliases/RealtimeEvent.md +36 -0
- package/docs/api/type-aliases/StreamChatMessageJson.md +9 -0
- package/docs/api/type-aliases/StreamEventJson.md +9 -0
- package/docs/api/type-aliases/StreamPlatform.md +9 -0
- package/docs/api/type-aliases/TwitchEventJson.md +9 -0
- package/docs/api/type-aliases/TwitchEventType.md +9 -0
- package/docs/api/type-aliases/UnsubscribeFunction.md +22 -0
- package/docs/api/type-aliases/ViewerPropertyJson.md +9 -0
- package/docs/api/type-aliases/ViewerPropertyType.md +9 -0
- package/docs/overview.md +160 -0
- 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
|