@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
JavaScript
"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;