npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@classytic/social

v0.3.1

Published

Unified social media provider SDK — YouTube, TikTok, Instagram, Facebook, LinkedIn, Telegram, WhatsApp, Twitter/X, Reddit. OAuth, video upload, publishing, scheduling.

Downloads

649

Readme

@classytic/social

One TypeScript SDK for nine social platforms. OAuth, posting, video upload, messaging — with batteries included for AI agents.

Node License: MIT

Platforms: YouTube · TikTok · Instagram · Facebook · LinkedIn · Telegram · WhatsApp · Twitter/X · Reddit


Why this package

  • One client, every platform. Configure once, post anywhere.
  • AI-ready. One import → typed tool definitions for OpenAI, Anthropic, MCP, and the Vercel AI SDK.
  • Type-safe end-to-end. Zod v4 schemas double as runtime validators, JSON Schema, and form descriptors.
  • Production-grade. Shared HTTP client with timeouts, retry, rate-limit handling, and SSRF guards.
  • Tree-shakeable. Per-provider subpath imports — only ship what you use.

Install

npm install @classytic/social
# YouTube provider needs googleapis as a peer dep:
npm install googleapis

Three-line quickstart

import { fromEnv } from '@classytic/social';

const client = fromEnv();                       // reads TWITTER_*, FACEBOOK_*, etc.
await client.post({ text: 'Hello, internet!' }); // fans out to every configured platform

That's it. Configure credentials via env vars, call post(), and the SDK handles auth, rate limits, retries, and platform quirks.


Configure explicitly

import { SocialClient } from '@classytic/social';

const client = SocialClient.create({
  twitter:  { clientId, clientSecret, tokens: { access_token } },
  linkedin: { clientId, clientSecret, tokens: { access_token }, authorUrn: 'urn:li:person:abc' },
  facebook: { appId, appSecret, tokens: { access_token }, pageId, pageAccessToken },
  telegram: { botToken: '123:abc', chatId: '@my-channel' },
});

// Fan out to all configured platforms
const result = await client.post({
  text: 'Hello world',
  media: [{ type: 'image', url: 'https://cdn.example.com/banner.png' }],
  link: 'https://example.com/launch',
});

console.log(result.allOk, result.results); // per-platform pass/fail breakdown

Per-platform calls

await client.twitter!.post({ text: 'Just a tweet' });
await client.telegram!.send('Hi from the bot');
await client.linkedin!.post({ text: 'Long-form content...' });
await client.youtube!.upload({ videoUrl: '...', title: 'Launch!' });

Video upload to multiple platforms

await client.upload({
  platforms: ['youtube', 'tiktok'],
  videoUrl: 'https://cdn.example.com/clip.mp4',
  title: 'Launch trailer',
  description: 'Out now.',
  privacy: 'public',
});

Direct messaging

await client.message({ platform: 'telegram', text: 'Build deployed' });
await client.message({ platform: 'whatsapp', to: '+15551234567', text: 'Verification code: 1234' });

AI agents — compose tools yourself

This package deliberately doesn't ship LLM-SDK-specific tool adapters. Tool format is an application concern that varies per stack and changes between SDK versions. Compose tools at the call site using primitives the package gives you: typed SocialClient methods + zod schemas from @classytic/social/schemas.

Vercel AI SDK v5

import { tool } from 'ai';
import { z } from 'zod';
import { SocialClient } from '@classytic/social';

const client = SocialClient.create({ /* ... */ });

export const tools = {
  twitter_post: tool({
    description: 'Post a tweet (≤280 characters)',
    inputSchema: z.object({
      text: z.string().max(280),
      replyToTweetId: z.string().optional(),
    }),
    execute: ({ text, replyToTweetId }) => client.twitter!.post({
      text,
      perPlatform: { twitter: { replyTo: replyToTweetId } },
    }),
  }),
  telegram_send: tool({
    description: 'Send a message via the configured Telegram bot',
    inputSchema: z.object({ text: z.string().min(1).max(4096) }),
    execute: ({ text }) => client.telegram!.send(text),
  }),
};

OpenAI / Anthropic / MCP

Generate JSON Schema from any zod schema with z.toJSONSchema() (zod v4 builtin):

import { z } from 'zod';

const twitterPostInput = z.object({
  text: z.string().max(280),
  replyToTweetId: z.string().optional(),
});

// OpenAI Chat Completions
const openaiTool = {
  type: 'function' as const,
  function: {
    name: 'twitter_post',
    description: 'Post a tweet',
    parameters: z.toJSONSchema(twitterPostInput),
  },
};

// Anthropic Messages
const anthropicTool = {
  name: 'twitter_post',
  description: 'Post a tweet',
  input_schema: z.toJSONSchema(twitterPostInput),
};

// MCP `tools/list`
const mcpTool = {
  name: 'twitter_post',
  description: 'Post a tweet',
  inputSchema: z.toJSONSchema(twitterPostInput),
};

// Then dispatch a tool call to the bound client:
async function dispatch(name: string, input: unknown) {
  switch (name) {
    case 'twitter_post': {
      const args = twitterPostInput.parse(input);
      return client.twitter!.post({
        text: args.text,
        perPlatform: { twitter: { replyTo: args.replyToTweetId } },
      });
    }
    // ... other tools
  }
}

That's 5 lines per tool. The package gives you:

  • Methodsclient.twitter!.post(...), client.telegram!.send(...) etc.
  • Schemas — pre-built zod v4 schemas in @classytic/social/schemas
  • CapabilitiesallCapabilities, providersWithCapability('messaging') for filtering

You compose them into whatever tool shape your LLM stack expects — and you control the input shape (e.g., flatten perPlatform.twitter.replyTo into a top-level replyToTweetId) without inheriting the package's opinions.


OAuth flows

Each sub-client exposes the standard OAuth lifecycle:

// 1. Build the consent URL
const url = await client.twitter!.authUrl('csrf-state-token');
// → redirect user

// 2. Exchange callback code (PKCE handled internally)
const tokens = await client.twitter!.exchangeCode(code, 'csrf-state-token');
// tokens are stored on `client.twitter.config.tokens`

// 3. Refresh later
const fresh = await client.twitter!.refresh();

Telegram and WhatsApp use static tokens — no OAuth required.


Subpath exports

Tree-shakeable entrypoints — import only what you need.

| Subpath | Purpose | |---|---| | @classytic/social | SocialClient, fromEnv, all provider classes, errors | | @classytic/social/client | Just the unified client + sub-clients | | @classytic/social/schemas | Zod v4 schemas + capability map (for arc, OpenAPI, MCP) | | @classytic/social/common | httpRequest, oauth2*, paginate, pollUntilComplete, assertPublicHttpUrl | | @classytic/social/webhooks | verifyMetaWebhook, verifyTelegramWebhook, verifyTwitterWebhook, verifyYouTubeWebhook, verifyHmac — timing-safe signature verification | | @classytic/social/<provider> | Direct access to a single provider class |


Schemas — design APIs from the SDK

Every credential, request input, and capability is a zod v4 schema. Generate JSON Schema, build forms, or validate at request boundaries:

import { z } from 'zod';
import {
  YouTubeUploadParamsSchema,
  TelegramSendMessageSchema,
  allCapabilities,
  providersWithCapability,
} from '@classytic/social/schemas';

// Validate input
const params = YouTubeUploadParamsSchema.parse(body);

// Generate OpenAPI / MCP tool spec
const jsonSchema = z.toJSONSchema(YouTubeUploadParamsSchema);

// Filter providers by feature
providersWithCapability('scheduling'); // → ['youtube', 'facebook']
providersWithCapability('messaging');  // → ['telegram', 'whatsapp']

// TikTok is NOT in the scheduling list — its Content Posting API has no
// schedule_time field. To schedule TikTok posts, queue them in your app
// and call `client.tiktok.upload(...)` at fire time. (Same for Twitter,
// Reddit, Telegram, WhatsApp, LinkedIn — see the capabilities matrix.)

Capabilities matrix

All flags below come from verified live behavior — every claim is gated by a contract test in tests/contracts/capabilities.contract.test.ts and exposed at runtime via provider.getMetadata().capabilities.

| Provider | Auth | Text | Photo | Video | Carousel | Delete | Edit | Schedule | Caveats | |---|---|---|---|---|---|---|---|---|---| | YouTube | OAuth2 | — | — | ✓ | — | ✓ | ✓ | ✓ | — | | TikTok | OAuth2 + PKCE | — | ✓ | ✓ | — | ✗ | ✗ | ✗ | no DELETE in Content Posting API; native scheduling silently ignored | | Instagram | OAuth2 (Meta) | — | ✓ | ✓ | ✓ | ✗ | ✗ | — | published-media DELETE returns Meta #1 — manual delete in IG app | | Facebook | OAuth2 (Meta) | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | — | | LinkedIn (member) | OAuth2 | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | — | member DELETE silently no-ops via API | | LinkedIn (org) | OAuth2 | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | — | requires Community Mgmt API approval, separate app | | Twitter/X | OAuth2 + PKCE | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | — | edit gated to X Premium | | Reddit | OAuth2 | ✓ | ✓ | ✓ | — | ✓ | ✓ | — | — | | Telegram | Bot Token | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | — | | WhatsApp | API Token | ✓ | ✓ | ✓ | — | ✗ | ✗ | — | 24h re-engagement window; sent messages can't be unsent |

Read these at runtime to decide UI affordances:

const meta = provider.getMetadata();

if (meta.capabilities.canDeletePost) {
  // show "Delete" button
}
if (meta.capabilities.caveats?.length) {
  // surface a warning chip listing them
}

provider.getMetadata().supportsScheduling is kept as a back-compat alias for capabilities.canSchedulePost. Prefer the new shape.


Error handling

All providers throw SocialError with a uniform shape:

import { SocialError } from '@classytic/social';

try {
  await client.twitter!.post({ text: 'Hi' });
} catch (e) {
  if (e instanceof SocialError) {
    console.log(e.provider);    // 'twitter'
    console.log(e.statusCode);  // 401, 429, 502, …
    console.log(e.errorCode);   // platform-specific code
    console.log(e.retryable);   // boolean | null
    console.log(e.retryAfter);  // seconds (for 429)
    console.log(e.hint);        // human-readable mitigation hint
  }
}

The unified client.post() / client.upload() never throw on per-platform failures — each result has ok: boolean and error? populated independently.


Custom HTTP / OAuth — build your own provider

import { httpRequest, oauth2ExchangeCode, paginate } from '@classytic/social/common';

// Same retry / timeout / 429 handling used internally
const { data } = await httpRequest('mastodon', {
  method: 'POST',
  url: 'https://mastodon.social/api/v1/statuses',
  bearer: accessToken,
  json: { status: 'Hello' },
  timeout: 30_000,
  retry: { attempts: 2 },
});

Webhook signature verification

Every social platform signs incoming webhook deliveries. Re-implementing HMAC + constant-time comparison in every consumer is a recipe for forgery bugs. Import the per-platform helper:

import {
  verifyMetaWebhook,
  verifyTelegramWebhook,
  verifyTwitterWebhook,
  verifyYouTubeWebhook,
} from '@classytic/social/webhooks';

// Meta (Facebook / Instagram / WhatsApp / Threads — all use X-Hub-Signature-256)
const ok = verifyMetaWebhook({
  rawBody: req.rawBody!,      // raw bytes, NOT the parsed body
  appSecret: process.env.META_APP_SECRET!,
  xHubSignature256: req.headers['x-hub-signature-256'] as string,
});
if (!ok) return reply.code(401).send();

| Provider | Algorithm | Encoding | Header | |---|---|---|---| | Meta (FB / IG / WhatsApp) | HMAC-SHA256 | hex | X-Hub-Signature-256: sha256=<hex> | | Twitter / X | HMAC-SHA256 | base64 | X-Twitter-Webhooks-Signature: sha256=<base64> | | YouTube PuSH | HMAC-SHA1 | hex | X-Hub-Signature: sha1=<hex> | | Telegram | string equality | — | X-Telegram-Bot-Api-Secret-Token: <secret> |

All comparisons use crypto.timingSafeEqual — the class of "your webhook is open to forgery" bugs is gone for every consumer that imports these.

Caveat: always pass the raw request bytes for HMAC-based providers. JSON-parsing then re-serializing the body (via JSON.stringify) changes whitespace/key order, breaking the signature. Use @fastify/raw-body or your framework's equivalent to capture the bytes.


Environment variables (fromEnv())

Set any subset — only matching platforms are enabled.

# YouTube
YOUTUBE_CLIENT_ID=...        YOUTUBE_CLIENT_SECRET=...
YOUTUBE_ACCESS_TOKEN=...     YOUTUBE_REFRESH_TOKEN=...

# Twitter
TWITTER_CLIENT_ID=...        TWITTER_CLIENT_SECRET=...
TWITTER_ACCESS_TOKEN=...     TWITTER_REFRESH_TOKEN=...

# Facebook (Pages)
FACEBOOK_APP_ID=...          FACEBOOK_APP_SECRET=...
FACEBOOK_ACCESS_TOKEN=...    FACEBOOK_PAGE_ID=...
FACEBOOK_PAGE_ACCESS_TOKEN=...

# Instagram
INSTAGRAM_APP_ID=...         INSTAGRAM_APP_SECRET=...
INSTAGRAM_ACCESS_TOKEN=...   INSTAGRAM_USER_ID=...

# LinkedIn
LINKEDIN_CLIENT_ID=...       LINKEDIN_CLIENT_SECRET=...
LINKEDIN_ACCESS_TOKEN=...    LINKEDIN_AUTHOR_URN=urn:li:person:...

# Reddit
REDDIT_CLIENT_ID=...         REDDIT_CLIENT_SECRET=...
REDDIT_USER_AGENT="web:my-app:v1.0 (by /u/yourname)"
REDDIT_ACCESS_TOKEN=...      REDDIT_REFRESH_TOKEN=...

# TikTok
TIKTOK_CLIENT_KEY=...        TIKTOK_CLIENT_SECRET=...
TIKTOK_ACCESS_TOKEN=...      TIKTOK_REFRESH_TOKEN=...

# Telegram (bot)
TELEGRAM_BOT_TOKEN=123:abc   TELEGRAM_CHAT_ID=@my-channel

# WhatsApp Business
WHATSAPP_ACCESS_TOKEN=...    WHATSAPP_BUSINESS_ACCOUNT_ID=...
WHATSAPP_PHONE_NUMBER_ID=...

Development

npm run build           # tsdown → dist/  (+ attw + publint validation)
npm run typecheck       # tsc --noEmit
npm test                # unit + contract tests (503 tests, ~2s)
npm run test:contracts  # contract probes only (149 tests, pins URLs/scopes/body shapes)
npm run test:e2e        # real-API publish tests (opt-in; needs RUN_E2E=1 + provider creds)
npm run test:watch      # watch mode

Test pyramid

| Tier | Files | Purpose | When it runs | |---|---|---|---| | Unit | tests/*.test.ts | logic + parsers + URL/body construction (mocked HTTP) | every npm test | | Contract | tests/contracts/*.contract.test.ts | pin URLs, scopes, body shapes, status enums, capabilities against platform docs | every npm test | | e2e | tests/e2e/*.e2e.test.ts | real publish → verify → delete → assert gone | opt-in (RUN_E2E=1 + per-provider creds) |

Contract probes are the gate that catches "platform docs changed but unit tests still mock the old shape" — exactly the bug class that escapes mocked-only suites.


License

MIT