velho
v0.2.5
Published
TypeScript-first Twitch connectivity toolkit
Maintainers
Readme
velho
TypeScript-first toolkit for building Twitch integrations. velho bundles authentication helpers, a typed Helix REST client, Twitch chat (IRC over WebSocket), and EventSub utilities—all guarded by Zod validation and shipped as dual ESM/CJS builds.
Table of Contents
Features
- OAuth helpers — Complete OAuth 2.0 flows including authorization code exchange, app-access (client credentials), and user token refresh with caching and configurable refresh leeway.
- Interactive CLI setup — One-command OAuth setup tool (
velho-setup) that handles the entire authorization flow and saves credentials automatically. - Typed Helix client — Minimal REST wrapper with typed
GET/POSThelpers, automatic retry on401, and rate-limit header parsing. - Chat (IRC over WebSocket) — Connect, join, listen, speak, respond to
PING, and auto-reconnect to Twitch chat. - EventSub tooling — Webhook signature verification + transport-agnostic handler, and a WebSocket client that maintains keepalive sessions.
- Developer experience — Zod validation at the edges, generated
.d.ts, exports map, sourcemaps, andtsupbundling for Node 18+.
Examples
Check out the example files to see velho in action:
- Quick Start Bot — Simple bot using the CLI setup tool
- Chat Command Bot — Advanced bot with multiple commands
- Authorization Code Flow — Manual OAuth implementation
- EventSub Webhook Server — Webhook event handling
- EventSub WebSocket Monitor — WebSocket event monitoring
For a complete getting started guide, see GETTING_STARTED.md.
Requirements
- Node.js 18.0.0 or newer (for native
fetch, Web Streams, and global WebSocket compatibility). - A Twitch application with a client ID/secret. For user scopes you will also need a refresh token issued via the OAuth authorization code flow.
Collecting Twitch credentials
Follow these Twitch developer console steps to gather the values velho expects:
- Visit the Twitch Developer Console and click Register Your Application (or pick an existing app).
- Give the application a name, choose the environment (
Website Integrationworks for most use cases), and add an OAuth redirect URL you control. Save. - Open the app from the list and copy the Client ID. Generate a New Secret to reveal the Client Secret—store it securely; Twitch only shows it once.
- Determine your Broadcaster ID (the Helix APIs label it
broadcaster_id). Either:- Use the Get Users endpoint:
curl -H "Client-ID: <client-id>" -H "Authorization: Bearer <token>" "https://api.twitch.tv/helix/users?login=<channel-name>"and copy theidfield, or - Query it through the Twitch CLI:
twitch api get users -q login=<channel-name>or - Use some 3rd party tool to query it, I'm not recomending any but simple Google search will give you answers
- Use the Get Users endpoint:
Keep these values in your .env file:
TWITCH_CLIENT_ID=...
TWITCH_CLIENT_SECRET=...
TWITCH_BROADCASTER_ID=...TWITCH_BROADCASTER_ID is optional unless you call Helix endpoints that require it (e.g. channel management) or set up EventSub subscriptions tied to a broadcaster.
Installation
npm install velho
# or
pnpm add velho
# or
yarn add velhoQuick Setup with CLI Tool
Get started instantly with the interactive OAuth setup:
# Create .env file with your Twitch credentials
echo "TWITCH_CLIENT_ID=your_client_id" > .env
echo "TWITCH_CLIENT_SECRET=your_client_secret" >> .env
# Run the interactive setup
npx velho-setupThis will:
- Start a local server for OAuth callback
- Open your browser to authorize the application
- Automatically save
TWITCH_BOT_REFRESH_TOKENandTWITCH_BOT_USERNAMEto your.envfile - Provide you with ready-to-use credentials
Quick Start
import { HelixClient, TwitchAuth } from "velho";
const auth = new TwitchAuth({
clientId: process.env.TWITCH_CLIENT_ID!,
clientSecret: process.env.TWITCH_CLIENT_SECRET!
});
const helix = new HelixClient({
clientId: process.env.TWITCH_CLIENT_ID!,
auth
});
const { data } = await helix.get("/streams", {
query: { first: 1 },
});
console.log(data);Environment Setup
Create a .env file in your project root with your Twitch credentials:
# .env
TWITCH_CLIENT_ID=your_client_id_here
TWITCH_CLIENT_SECRET=your_client_secret_here
TWITCH_BROADCASTER_ID=your_broadcaster_id_here # optional for channel-scoped APIsGet these credentials from the Twitch Developer Console.
The authentication helper caches tokens per scope set. The Helix client retries once on 401 by invalidating and refreshing the token automatically.
Modules
Authentication
import { TwitchAuth } from "velho";
const auth = new TwitchAuth({
clientId: "...",
clientSecret: "...",
refreshLeewaySeconds: 120,
});
// App access token (client credentials flow)
const appToken = await auth.getAppAccessToken(["channel:read:subscriptions"]);
// User access token from refresh token
const userToken = await auth.getUserAccessToken({ refreshToken: "..." });
// Exchange authorization code for tokens (OAuth 2.0 authorization code flow)
const newUserToken = await auth.exchangeAuthorizationCode({
code: "authorization_code_from_callback",
redirectUri: "http://localhost:3000/auth/callback",
});Key points:
- Complete OAuth 2.0 support: Authorization code exchange, client credentials, and refresh token flows
- Tokens are cached per scope (app) or refresh token (user) until they are within the configurable refresh leeway
forceRefreshbypasses the cache when you know the token is invalid- Provide a custom
fetchFnif your runtime does not expose globalfetch(e.g., older Node versions or testing environments)
Helix Client
import { HelixClient, HelixRequestError } from "velho";
import { z } from "zod";
const helix = new HelixClient({
clientId: "...",
auth,
});
const channelsSchema = z.object({
data: z.array(
z.object({
broadcaster_id: z.string(),
broadcaster_name: z.string(),
broadcaster_language: z.string(),
})
),
});
try {
const response = await helix.get("/channels", {
query: { broadcaster_id: ["123456"] },
schema: channelsSchema,
});
console.log(response.data.data[0]);
console.log("Remaining quota:", response.rateLimit.remaining);
} catch (error) {
if (error instanceof HelixRequestError) {
console.error(error.status, error.details);
}
}Highlights:
- Methods:
get,post,patch,put,delete. schema(optional) enforces response shape with Zod.tokenoverrides the default token strategy per request (app vs user).- Rate-limit headers (
ratelimit-*) are parsed intoHelixRateLimiton every response.
Chat (IRC over WS)
import { TwitchChatClient } from "velho";
const chat = new TwitchChatClient({
username: "my_bot",
token: process.env.TWITCH_CHAT_TOKEN!,
reconnect: true,
autoJoin: ["#velho"],
});
await chat.connect();
chat.on("ready", () => console.log("Chat ready"));
chat.on("message", (message) => {
console.log(`[${message.channel}] <${message.username}> ${message.text}`);
});
chat.say("#velho", "Hello Twitch!");Capabilities:
- Emits strongly typed events:
ready,message,notice,join,part,reconnect,close,raw,error. - Handles
PING/PONGautomatically. listen(listener)convenience subscribes to messages and returns an unsubscribe callback.- Minimal reconnect strategy with configurable delay; bring your own exponential backoff if required.
EventSub
Webhook Utilities
import { handleEventSubWebhook } from "velho";
import { z } from "zod";
const cheerSchema = z.object({
broadcaster_user_id: z.string(),
is_anonymous: z.boolean(),
bits: z.number(),
});
export async function twitchWebhookHandler(req, res) {
const body = await getRawBody(req);
const result = await handleEventSubWebhook({
headers: req.headers,
body,
}, {
secret: process.env.TWITCH_EVENTSUB_SECRET!,
eventSchema: cheerSchema,
onNotification: async ({ subscription, event }) => {
console.log(subscription.type, event.bits);
},
onRevocation: ({ subscription }) => {
console.warn("Revoked:", subscription);
},
});
res.writeHead(result.status, result.headers);
res.end(result.body);
}- Agnostic of specific HTTP frameworks—just provide raw headers and body string.
- Signature verification uses HMAC-SHA256 and constant-time comparison.
webhook_callback_verificationsupport returns the challenge automatically.
WebSocket Client
import { EventSubWebSocketClient } from "velho";
const wsClient = new EventSubWebSocketClient({ autoReconnect: true });
await wsClient.connect();
wsClient.on("connected", (session) => {
console.log("Session ID", session.id);
});
wsClient.on("notification", ({ subscription, event }) => {
console.log(subscription.type, event);
});- Maintains the active session, reconnecting as Twitch instructs.
- Resets a keepalive timer to detect silent disconnects.
- Leaves subscription creation to your Helix client; use the maintained session ID in Helix EventSub subscription requests.
Design & DX
- Type Safety Everywhere — Responses validated with Zod when schemas are provided; library types guard event payloads and tokens.
- Dual Build Output — Distributed as ESM (
dist/index.mjs) and CommonJS (dist/index.cjs) withexportsmap and.d.tsfiles. - First-Class Node 18+ — Targets modern runtime features; polyfills only required when you target older environments.
- Extensible Fetch — Inject your own
fetchfor testing or alternative runtimes (e.g., undici with instrumentation).
Development
# Install dependencies
git clone <repo>
cd velho
npm install
# Type-check
npm run check
# Build (ESM+CJS+d.ts)
npm run build- Source resides in
src/. - Builds output to
dist/viatsupand cleans the folder before each run. - No test suite yet; consider combining this package with your integration tests or add mocks in
tests/.
Contributing
We welcome contributions! See CONTRIBUTING.md for the full workflow, coding standards, and release process.
License
MIT © 2024 Joni Juntto
