/**
* Scaffolded Twitch Helix OpenAPI client (dependency-free ESM).
*
* Source: openapi_client_scaffold against
* https://raw.githubusercontent.com/DmitryScaletta/twitch-api-swagger/main/openapi.json
* Slugs: get_users, get_streams, send_chat_message, get_chatters,
* modify_channel_information, search_categories
* Auth: integration — createAuthenticatedFetch('twitch') for Authorization.
* Helix also requires Client-Id; the package wrapper merges that via input.headers.
*
* Spec is community/unofficial; host approval stays in the account security UI.
*
* Note: scaffold emitted createAuthenticatedFetch(kody, "twitch"); runtime expects
* createAuthenticatedFetch(providerName) — adapted to match Spotify/package convention.
*/
import { createAuthenticatedFetch } from 'kody:runtime'
export type QueryValue = string | number | boolean | null | undefined
export type QueryInput = Record<string, QueryValue | QueryValue[]>
export type ScaffoldInput = {
params?: Record<string, unknown>
query?: QueryInput
headers?: Record<string, string>
body?: unknown
}
export type ScaffoldOptions = {
/** Bypass integration auth (tests). */
fetchImpl?: typeof fetch
}
const API_BASE_URL = 'https://api.twitch.tv/helix'
const TWITCH_PROVIDER = 'twitch'
export function buildUrl(pathTemplate: string, params: Record<string, unknown> = {}): string {
return API_BASE_URL + pathTemplate.replace(/\{([^}]+)\}/g, (_match, name: string) => {
const value = params[name]
if (value === undefined || value === null) {
throw new Error(`Missing required path parameter: ${name}`)
}
return encodeURIComponent(String(value))
})
}
export function appendQuery(url: string, query: QueryInput = {}): string {
const search = new URLSearchParams()
for (const [key, value] of Object.entries(query)) {
if (value === undefined || value === null) continue
if (Array.isArray(value)) {
for (const item of value) {
if (item === undefined || item === null) continue
search.append(key, String(item))
}
continue
}
search.append(key, String(value))
}
const qs = search.toString()
return qs ? `${url}?${qs}` : url
}
function mergeHeaders(
userHeaders: Record<string, string> | undefined,
authHeaders: Record<string, string>,
): Record<string, string> {
const merged: Record<string, string> = { ...(userHeaders ?? {}) }
for (const [key, value] of Object.entries(authHeaders)) {
const lower = key.toLowerCase()
for (const existing of Object.keys(merged)) {
if (existing.toLowerCase() === lower) delete merged[existing]
}
merged[key] = value
}
return merged
}
function hasHeader(headers: Record<string, string>, name: string): boolean {
const lower = name.toLowerCase()
return Object.keys(headers).some((key) => key.toLowerCase() === lower)
}
let __authedFetch: typeof fetch | null = null
async function resolveFetch(fetchImpl?: typeof fetch): Promise<typeof fetch> {
if (fetchImpl) return fetchImpl
if (!__authedFetch) {
__authedFetch = await createAuthenticatedFetch(TWITCH_PROVIDER)
}
return __authedFetch
}
/** Integration auth supplies Authorization; Client-Id is merged by package callers. */
function authHeaders(): Record<string, string> {
return {}
}
async function call(
method: string,
pathTemplate: string,
input: ScaffoldInput = {},
options: ScaffoldOptions = {},
): Promise<Response> {
const params = input.params ?? {}
const url = appendQuery(buildUrl(pathTemplate, params), input.query)
const headers = mergeHeaders(input.headers, authHeaders())
let body: string | undefined
if (input.body !== undefined) {
body = JSON.stringify(input.body)
if (!hasHeader(headers, 'content-type')) headers['content-type'] = 'application/json'
}
const fetchImpl = await resolveFetch(options.fetchImpl)
return fetchImpl(url, { method, headers, body })
}
/** GET /users — get_users */
export async function getUsers(
input: ScaffoldInput = {},
options: ScaffoldOptions = {},
): Promise<Response> {
return call('GET', '/users', input, options)
}
/** GET /streams — get_streams */
export async function getStreams(
input: ScaffoldInput = {},
options: ScaffoldOptions = {},
): Promise<Response> {
return call('GET', '/streams', input, options)
}
/** POST /chat/messages — send_chat_message */
export async function sendChatMessage(
input: ScaffoldInput = {},
options: ScaffoldOptions = {},
): Promise<Response> {
return call('POST', '/chat/messages', input, options)
}
/** GET /chat/chatters — get_chatters */
export async function getChatters(
input: ScaffoldInput = {},
options: ScaffoldOptions = {},
): Promise<Response> {
return call('GET', '/chat/chatters', input, options)
}
/** PATCH /channels — modify_channel_information */
export async function modifyChannelInformation(
input: ScaffoldInput = {},
options: ScaffoldOptions = {},
): Promise<Response> {
return call('PATCH', '/channels', input, options)
}
/** GET /search/categories — search_categories */
export async function searchCategories(
input: ScaffoldInput = {},
options: ScaffoldOptions = {},
): Promise<Response> {
return call('GET', '/search/categories', input, options)
}
/**
* Escape hatch for Helix paths not in the curated OpenAPI selection.
* Path is relative to https://api.twitch.tv/helix (leading slash required).
*/
export async function rawHelixRequest(
path: string,
input: {
method?: string
query?: QueryInput
headers?: Record<string, string>
body?: unknown
fetchImpl?: typeof fetch
} = {},
): Promise<Response> {
if (!path.startsWith('/')) {
throw new Error('Helix path must start with "/" (relative to api.twitch.tv/helix).')
}
if (path.startsWith('http://') || path.startsWith('https://')) {
throw new Error('Pass a Helix-relative path, not an absolute URL.')
}
const method = (input.method ?? (input.body === undefined ? 'GET' : 'POST')).toUpperCase()
return call(
method,
path,
{
query: input.query,
headers: input.headers,
body: input.body,
},
{ fetchImpl: input.fetchImpl },
)
}
export { API_BASE_URL, mergeHeaders, TWITCH_PROVIDER }