UNPKG

@nekiro/kick-api

Version:

Efortlessly query kick.com api using easy to use interface with properly typed responses.

321 lines (320 loc) 14.7 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.KickClient = void 0; const categories_1 = require("./modules/categories"); const channels_1 = require("./modules/channels"); const livestreams_1 = require("./modules/livestreams"); const chat_1 = require("./modules/chat"); const errors_1 = require("./errors"); const crypto_1 = require("crypto"); class KickClient { constructor(config) { this.token = null; this.tokenPromise = null; this.config = Object.assign({ baseUrl: "https://api.kick.com", oauthUrl: "https://id.kick.com" }, config); this.categories = new categories_1.CategoriesModule(this); this.channels = new channels_1.ChannelsModule(this); this.livestreams = new livestreams_1.LivestreamsModule(this); this.chat = new chat_1.ChatModule(this); } generatePKCEParams() { const codeVerifier = (0, crypto_1.randomBytes)(32).toString("base64url"); const codeChallenge = (0, crypto_1.createHash)("sha256").update(codeVerifier).digest("base64url"); const state = (0, crypto_1.randomBytes)(16).toString("hex"); return { codeVerifier, codeChallenge, state, }; } getAuthorizationUrl(params, scopes = ["public"]) { if (!this.config.redirectUri) { throw new Error("redirectUri is required for user authentication flow. For server-to-server, tokens are handled automatically."); } const url = new URL(`${this.config.oauthUrl}/oauth/authorize`); url.searchParams.set("response_type", "code"); url.searchParams.set("client_id", this.config.clientId); url.searchParams.set("redirect_uri", this.config.redirectUri); url.searchParams.set("scope", scopes.join(" ")); url.searchParams.set("code_challenge", params.codeChallenge); url.searchParams.set("code_challenge_method", "S256"); if (params.state) { url.searchParams.set("state", params.state); } return url.toString(); } exchangeCodeForToken(tokenRequest) { return __awaiter(this, void 0, void 0, function* () { if (!this.config.redirectUri) { throw new Error("redirectUri is required for authorization code flow. For server-to-server, tokens are handled automatically."); } try { const body = new URLSearchParams({ grant_type: "authorization_code", client_id: this.config.clientId, client_secret: this.config.clientSecret, redirect_uri: this.config.redirectUri, code_verifier: tokenRequest.codeVerifier, code: tokenRequest.code, }); const response = yield fetch(`${this.config.oauthUrl}/oauth/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: body.toString(), }); if (this.config.debug) { console.log("🔍 Debug - OAuth Token Request:"); console.log("URL:", `${this.config.oauthUrl}/oauth/token`); console.log("Body:", body.toString()); } if (!response.ok) { let responseBody; try { responseBody = yield response.text(); try { responseBody = JSON.parse(responseBody); } catch (_a) { } } catch (_b) { responseBody = "Unable to read response body"; } if (this.config.debug) { console.log("🔍 Debug - OAuth Error Response:"); console.log("Status:", response.status, response.statusText); console.log("Body:", responseBody); } throw new errors_1.KickOAuthError(`Token exchange failed: ${response.status} ${response.statusText}`, response.status, responseBody); } const data = yield response.json(); this.token = { accessToken: data.access_token, tokenType: data.token_type, expiresIn: data.expires_in, refreshToken: data.refresh_token, scope: data.scope, expiresAt: Date.now() + data.expires_in * 1000, }; return this.token; } catch (error) { if (error instanceof errors_1.KickOAuthError) { throw error; } throw new errors_1.KickNetworkError("Failed to connect to OAuth endpoint", error); } }); } setToken(token) { this.token = token; } getAccessToken() { return __awaiter(this, void 0, void 0, function* () { if (this.tokenPromise) { return this.tokenPromise; } if (this.token && this.isTokenValid()) { return this.token.accessToken; } this.tokenPromise = this.autoRefreshToken(); try { const token = yield this.tokenPromise; return token; } finally { this.tokenPromise = null; } }); } autoRefreshToken() { return __awaiter(this, void 0, void 0, function* () { var _a; if ((_a = this.token) === null || _a === void 0 ? void 0 : _a.refreshToken) { try { return yield this.refreshAccessToken(); } catch (error) { if (this.config.debug) { console.log("🔄 Token refresh failed, getting new token:", error.message); } } } if (!this.config.redirectUri) { return yield this.getClientCredentialsToken(); } throw new errors_1.KickOAuthError("No valid token available. For user authentication, use exchangeCodeForToken() first. For server-to-server, omit redirectUri from config.", 401); }); } getClientCredentialsToken() { return __awaiter(this, void 0, void 0, function* () { try { const body = new URLSearchParams({ grant_type: "client_credentials", client_id: this.config.clientId, client_secret: this.config.clientSecret, }); const response = yield fetch(`${this.config.oauthUrl}/oauth/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: body.toString(), }); if (this.config.debug) { console.log("🔍 Debug - Client Credentials Request:"); console.log("URL:", `${this.config.oauthUrl}/oauth/token`); console.log("Body:", body.toString()); } if (!response.ok) { let responseBody; try { responseBody = yield response.text(); try { responseBody = JSON.parse(responseBody); } catch (_a) { } } catch (_b) { responseBody = "Unable to read response body"; } if (this.config.debug) { console.log("🔍 Debug - Client Credentials Error:"); console.log("Status:", response.status, response.statusText); console.log("Body:", responseBody); } throw new errors_1.KickOAuthError(`Client credentials token request failed: ${response.status} ${response.statusText}`, response.status, responseBody); } const data = yield response.json(); this.token = { accessToken: data.access_token, tokenType: data.token_type, expiresIn: data.expires_in, refreshToken: data.refresh_token, scope: data.scope, expiresAt: Date.now() + data.expires_in * 1000, }; return this.token.accessToken; } catch (error) { if (error instanceof errors_1.KickOAuthError) { throw error; } throw new errors_1.KickNetworkError("Failed to get client credentials token", error); } }); } isTokenValid() { if (!this.token) return false; return Date.now() < this.token.expiresAt - 60000; } refreshAccessToken() { return __awaiter(this, void 0, void 0, function* () { var _a; if (!((_a = this.token) === null || _a === void 0 ? void 0 : _a.refreshToken)) { throw new errors_1.KickOAuthError("No refresh token available", 401); } try { const body = new URLSearchParams({ grant_type: "refresh_token", client_id: this.config.clientId, client_secret: this.config.clientSecret, refresh_token: this.token.refreshToken, }); const response = yield fetch(`${this.config.oauthUrl}/oauth/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: body.toString(), }); if (!response.ok) { let responseBody; try { responseBody = yield response.text(); try { responseBody = JSON.parse(responseBody); } catch (_b) { } } catch (_c) { responseBody = "Unable to read response body"; } if (this.config.debug) { console.log("🔍 Debug - OAuth Refresh Error:"); console.log("Status:", response.status, response.statusText); console.log("Body:", responseBody); } throw new errors_1.KickOAuthError(`Token refresh failed: ${response.status} ${response.statusText}`, response.status, responseBody); } const data = yield response.json(); this.token = { accessToken: data.access_token, tokenType: data.token_type, expiresIn: data.expires_in, refreshToken: data.refresh_token || this.token.refreshToken, scope: data.scope, expiresAt: Date.now() + data.expires_in * 1000, }; return this.token.accessToken; } catch (error) { if (error instanceof errors_1.KickOAuthError) { throw error; } throw new errors_1.KickNetworkError("Failed to refresh token", error); } }); } request(endpoint_1) { return __awaiter(this, arguments, void 0, function* (endpoint, options = {}) { try { const accessToken = yield this.getAccessToken(); const url = `${this.config.baseUrl}${endpoint}`; const response = yield fetch(url, Object.assign(Object.assign({}, options), { headers: Object.assign({ Authorization: `Bearer ${accessToken}`, Accept: "application/json", "Content-Type": "application/json" }, options.headers) })); if (!response.ok) { let responseBody; try { responseBody = yield response.text(); try { responseBody = JSON.parse(responseBody); } catch (_a) { } } catch (_b) { responseBody = "Unable to read response body"; } const headers = Object.fromEntries(response.headers.entries()); if (this.config.debug) { console.log("🔍 Debug - API Error Response:"); console.log("Status:", response.status, response.statusText); console.log("Headers:", headers); console.log("Body:", responseBody); console.log("Endpoint:", endpoint); } throw (0, errors_1.createKickError)(response.status, response.statusText, responseBody, headers, endpoint); } const json = yield response.json(); return json.data; } catch (error) { if (error instanceof Error && error.constructor.name.startsWith("Kick")) { throw error; } throw new errors_1.KickNetworkError(`Request to ${endpoint} failed`, error); } }); } } exports.KickClient = KickClient;