@classytic/social 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +368 -0
- package/dist/base-Bw7e52V8.mjs +246 -0
- package/dist/base-Bw7e52V8.mjs.map +1 -0
- package/dist/base-DBtKFiSX.d.mts +226 -0
- package/dist/base-DBtKFiSX.d.mts.map +1 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/client/index.d.mts +44 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +154 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/common/index.d.mts +3 -0
- package/dist/common/index.mjs +7 -0
- package/dist/contracts-Cdwa4zlg.d.mts +121 -0
- package/dist/contracts-Cdwa4zlg.d.mts.map +1 -0
- package/dist/contracts-lCa069IK.mjs +221 -0
- package/dist/contracts-lCa069IK.mjs.map +1 -0
- package/dist/env-Bl0cwwjC.mjs +955 -0
- package/dist/env-Bl0cwwjC.mjs.map +1 -0
- package/dist/env-DxOZHf0p.d.mts +394 -0
- package/dist/env-DxOZHf0p.d.mts.map +1 -0
- package/dist/errors-Cm6LeKf7.mjs +32 -0
- package/dist/errors-Cm6LeKf7.mjs.map +1 -0
- package/dist/facebook-l_4CghaA.mjs +95 -0
- package/dist/facebook-l_4CghaA.mjs.map +1 -0
- package/dist/http-DpcLSR1M.mjs +197 -0
- package/dist/http-DpcLSR1M.mjs.map +1 -0
- package/dist/index.d.mts +42 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +71 -0
- package/dist/index.mjs.map +1 -0
- package/dist/instagram-BGaeUFU2.mjs +90 -0
- package/dist/instagram-BGaeUFU2.mjs.map +1 -0
- package/dist/linkedin-70whtVKa.mjs +101 -0
- package/dist/linkedin-70whtVKa.mjs.map +1 -0
- package/dist/meta-D3vcJU1c.mjs +126 -0
- package/dist/meta-D3vcJU1c.mjs.map +1 -0
- package/dist/pkce-jq5II68b.mjs +72 -0
- package/dist/pkce-jq5II68b.mjs.map +1 -0
- package/dist/polling-DZ1apXtA.mjs +25 -0
- package/dist/polling-DZ1apXtA.mjs.map +1 -0
- package/dist/providers/facebook.d.mts +135 -0
- package/dist/providers/facebook.d.mts.map +1 -0
- package/dist/providers/facebook.mjs +450 -0
- package/dist/providers/facebook.mjs.map +1 -0
- package/dist/providers/instagram.d.mts +122 -0
- package/dist/providers/instagram.d.mts.map +1 -0
- package/dist/providers/instagram.mjs +496 -0
- package/dist/providers/instagram.mjs.map +1 -0
- package/dist/providers/linkedin.d.mts +145 -0
- package/dist/providers/linkedin.d.mts.map +1 -0
- package/dist/providers/linkedin.mjs +574 -0
- package/dist/providers/linkedin.mjs.map +1 -0
- package/dist/providers/reddit.d.mts +102 -0
- package/dist/providers/reddit.d.mts.map +1 -0
- package/dist/providers/reddit.mjs +657 -0
- package/dist/providers/reddit.mjs.map +1 -0
- package/dist/providers/telegram.d.mts +139 -0
- package/dist/providers/telegram.d.mts.map +1 -0
- package/dist/providers/telegram.mjs +517 -0
- package/dist/providers/telegram.mjs.map +1 -0
- package/dist/providers/tiktok.d.mts +116 -0
- package/dist/providers/tiktok.d.mts.map +1 -0
- package/dist/providers/tiktok.mjs +676 -0
- package/dist/providers/tiktok.mjs.map +1 -0
- package/dist/providers/twitter.d.mts +150 -0
- package/dist/providers/twitter.d.mts.map +1 -0
- package/dist/providers/twitter.mjs +628 -0
- package/dist/providers/twitter.mjs.map +1 -0
- package/dist/providers/whatsapp.d.mts +79 -0
- package/dist/providers/whatsapp.d.mts.map +1 -0
- package/dist/providers/whatsapp.mjs +376 -0
- package/dist/providers/whatsapp.mjs.map +1 -0
- package/dist/providers/youtube.d.mts +153 -0
- package/dist/providers/youtube.d.mts.map +1 -0
- package/dist/providers/youtube.mjs +902 -0
- package/dist/providers/youtube.mjs.map +1 -0
- package/dist/reddit-B10kS4Se.mjs +126 -0
- package/dist/reddit-B10kS4Se.mjs.map +1 -0
- package/dist/schemas/index.d.mts +819 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +31 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/security-BXhfebWm.d.mts +338 -0
- package/dist/security-BXhfebWm.d.mts.map +1 -0
- package/dist/shared-Fvc6xQku.mjs +100 -0
- package/dist/shared-Fvc6xQku.mjs.map +1 -0
- package/dist/telegram-FaUHpZgB.mjs +107 -0
- package/dist/telegram-FaUHpZgB.mjs.map +1 -0
- package/dist/tiktok-B_bMk4G-.mjs +94 -0
- package/dist/tiktok-B_bMk4G-.mjs.map +1 -0
- package/dist/twitter-BC22zfuc.mjs +98 -0
- package/dist/twitter-BC22zfuc.mjs.map +1 -0
- package/dist/types-BFE4psYI.d.mts +102 -0
- package/dist/types-BFE4psYI.d.mts.map +1 -0
- package/dist/types-Bv27tcT0.d.mts +230 -0
- package/dist/types-Bv27tcT0.d.mts.map +1 -0
- package/dist/types-BwkKyqpi.d.mts +253 -0
- package/dist/types-BwkKyqpi.d.mts.map +1 -0
- package/dist/types-CJrHMDV9.mjs +27 -0
- package/dist/types-CJrHMDV9.mjs.map +1 -0
- package/dist/types-ClbVc2rc.d.mts +117 -0
- package/dist/types-ClbVc2rc.d.mts.map +1 -0
- package/dist/types-D91N16Ym.d.mts +242 -0
- package/dist/types-D91N16Ym.d.mts.map +1 -0
- package/dist/types-DfLp_ibQ.d.mts +178 -0
- package/dist/types-DfLp_ibQ.d.mts.map +1 -0
- package/dist/types-DfjDgEoJ.d.mts +88 -0
- package/dist/types-DfjDgEoJ.d.mts.map +1 -0
- package/dist/types-Dp5Z9VBr.mjs +23 -0
- package/dist/types-Dp5Z9VBr.mjs.map +1 -0
- package/dist/types-hriBJTsU.d.mts +129 -0
- package/dist/types-hriBJTsU.d.mts.map +1 -0
- package/dist/types-rn6UuLL8.d.mts +184 -0
- package/dist/types-rn6UuLL8.d.mts.map +1 -0
- package/dist/whatsapp-CFp7ryR4.mjs +101 -0
- package/dist/whatsapp-CFp7ryR4.mjs.map +1 -0
- package/dist/youtube-Bs0fdY7H.mjs +98 -0
- package/dist/youtube-Bs0fdY7H.mjs.map +1 -0
- package/package.json +148 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@classytic/social` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
While the package is on `0.x.y`, minor bumps may include breaking changes;
|
|
9
|
+
patch bumps are always backwards-compatible. We'll bump to `1.0.0` once the
|
|
10
|
+
public surface stabilizes.
|
|
11
|
+
|
|
12
|
+
## [0.1.0] — 2026-05-09
|
|
13
|
+
|
|
14
|
+
Initial public release. Unified social media SDK for 9 platforms — YouTube,
|
|
15
|
+
TikTok, Instagram, Facebook, LinkedIn, Telegram, WhatsApp, Twitter/X, Reddit.
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- **`SocialClient` unified façade** — credential-bound wrapper exposing each
|
|
20
|
+
provider as a typed sub-client (`client.twitter`, `client.facebook`, …) plus
|
|
21
|
+
multi-platform `post()` / `upload()` / `message()` fan-out.
|
|
22
|
+
- **`fromEnv()` auto-config** — instantiate from `process.env` using standard
|
|
23
|
+
`<PLATFORM>_*` variable names. Only platforms with required vars are enabled.
|
|
24
|
+
- **9 provider classes** — direct, low-level access to each platform's full
|
|
25
|
+
surface (OAuth, CRUD, upload, messaging, analytics where supported).
|
|
26
|
+
- **Factory `createRegistry({ port })`** — dynamic dispatch by provider name
|
|
27
|
+
for advanced multi-tenant scenarios.
|
|
28
|
+
|
|
29
|
+
### Subpath exports
|
|
30
|
+
|
|
31
|
+
- `@classytic/social` — main entrypoint (client, providers, errors)
|
|
32
|
+
- `@classytic/social/client` — just the unified client + sub-clients
|
|
33
|
+
- `@classytic/social/schemas` — zod v4 schemas + `allCapabilities` map +
|
|
34
|
+
`providersWithCapability(key)` for consumers (e.g. `@classytic/arc`) that
|
|
35
|
+
generate APIs / OpenAPI specs / MCP tools from schemas
|
|
36
|
+
- `@classytic/social/common` — shared helpers exposed publicly:
|
|
37
|
+
- `httpRequest<T>()` — fetch wrapper with timeout, exponential-backoff retry,
|
|
38
|
+
`Retry-After` honoring, structured `SocialError` throwing
|
|
39
|
+
- `oauth2ExchangeCode` / `oauth2RefreshToken` / `buildAuthUrl` — RFC 6749
|
|
40
|
+
- `metaExchangeLongLived` / `metaRefreshLongLived*` — Meta two-step flow
|
|
41
|
+
- `PkceStore` / `generateCodeVerifier` / `generateCodeChallenge` — PKCE
|
|
42
|
+
- `paginate()` — generic cursor pagination → `AsyncIterable` with `.toArray()` / `.take(n)`
|
|
43
|
+
- `pollUntilComplete()` — poll-with-timeout for async jobs
|
|
44
|
+
- `assertPublicHttpUrl()` — SSRF guard for user-supplied URLs
|
|
45
|
+
- `redactSecrets()` — safe object-tree logging
|
|
46
|
+
- `@classytic/social/<provider>` — direct access to one provider class
|
|
47
|
+
|
|
48
|
+
### Cross-cutting
|
|
49
|
+
|
|
50
|
+
- **`SocialError`** — uniform error class with `provider`, `statusCode`,
|
|
51
|
+
`errorCode`, `hint`, `retryable`, `retryAfter`, `originalError`. Every provider
|
|
52
|
+
populates these consistently via the shared HTTP layer.
|
|
53
|
+
- **Capability declarations** per provider via `*Capabilities` constants
|
|
54
|
+
(auth type, posting, upload, messaging, scheduling, deletion, listing,
|
|
55
|
+
analytics, environments, PKCE).
|
|
56
|
+
- **Per-provider rate-limit / file-size metadata** via `*Info` constants
|
|
57
|
+
in the schemas subpath.
|
|
58
|
+
- **Tree-shakeable** — `sideEffects: false`, per-provider subpath exports.
|
|
59
|
+
- **325 tests, build-validated with `attw` + `publint`.**
|
|
60
|
+
|
|
61
|
+
### Examples
|
|
62
|
+
|
|
63
|
+
5 runnable scripts under [`examples/`](./examples/) covering multi-platform
|
|
64
|
+
posting, Express OAuth flow, Vercel AI SDK tool composition (composed at the
|
|
65
|
+
call site — no package adapters), scheduled posts, and Telegram bot flow.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Classytic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# @classytic/social
|
|
2
|
+
|
|
3
|
+
> One TypeScript SDK for nine social platforms. OAuth, posting, video upload, messaging — with batteries included for AI agents.
|
|
4
|
+
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
**Platforms:** YouTube · TikTok · Instagram · Facebook · LinkedIn · Telegram · WhatsApp · Twitter/X · Reddit
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why this package
|
|
13
|
+
|
|
14
|
+
- **One client, every platform.** Configure once, post anywhere.
|
|
15
|
+
- **AI-ready.** One import → typed tool definitions for OpenAI, Anthropic, MCP, and the Vercel AI SDK.
|
|
16
|
+
- **Type-safe end-to-end.** Zod v4 schemas double as runtime validators, JSON Schema, and form descriptors.
|
|
17
|
+
- **Production-grade.** Shared HTTP client with timeouts, retry, rate-limit handling, and SSRF guards.
|
|
18
|
+
- **Tree-shakeable.** Per-provider subpath imports — only ship what you use.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @classytic/social
|
|
26
|
+
# YouTube provider needs googleapis as a peer dep:
|
|
27
|
+
npm install googleapis
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Three-line quickstart
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { fromEnv } from '@classytic/social';
|
|
36
|
+
|
|
37
|
+
const client = fromEnv(); // reads TWITTER_*, FACEBOOK_*, etc.
|
|
38
|
+
await client.post({ text: 'Hello, internet!' }); // fans out to every configured platform
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it. Configure credentials via env vars, call `post()`, and the SDK handles auth, rate limits, retries, and platform quirks.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Configure explicitly
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { SocialClient } from '@classytic/social';
|
|
49
|
+
|
|
50
|
+
const client = SocialClient.create({
|
|
51
|
+
twitter: { clientId, clientSecret, tokens: { access_token } },
|
|
52
|
+
linkedin: { clientId, clientSecret, tokens: { access_token }, authorUrn: 'urn:li:person:abc' },
|
|
53
|
+
facebook: { appId, appSecret, tokens: { access_token }, pageId, pageAccessToken },
|
|
54
|
+
telegram: { botToken: '123:abc', chatId: '@my-channel' },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Fan out to all configured platforms
|
|
58
|
+
const result = await client.post({
|
|
59
|
+
text: 'Hello world',
|
|
60
|
+
media: [{ type: 'image', url: 'https://cdn.example.com/banner.png' }],
|
|
61
|
+
link: 'https://example.com/launch',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
console.log(result.allOk, result.results); // per-platform pass/fail breakdown
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Per-platform calls
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
await client.twitter!.post({ text: 'Just a tweet' });
|
|
71
|
+
await client.telegram!.send('Hi from the bot');
|
|
72
|
+
await client.linkedin!.post({ text: 'Long-form content...' });
|
|
73
|
+
await client.youtube!.upload({ videoUrl: '...', title: 'Launch!' });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Video upload to multiple platforms
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
await client.upload({
|
|
80
|
+
platforms: ['youtube', 'tiktok'],
|
|
81
|
+
videoUrl: 'https://cdn.example.com/clip.mp4',
|
|
82
|
+
title: 'Launch trailer',
|
|
83
|
+
description: 'Out now.',
|
|
84
|
+
privacy: 'public',
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Direct messaging
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
await client.message({ platform: 'telegram', text: 'Build deployed' });
|
|
92
|
+
await client.message({ platform: 'whatsapp', to: '+15551234567', text: 'Verification code: 1234' });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## AI agents — compose tools yourself
|
|
98
|
+
|
|
99
|
+
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`.
|
|
100
|
+
|
|
101
|
+
### Vercel AI SDK v5
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { tool } from 'ai';
|
|
105
|
+
import { z } from 'zod';
|
|
106
|
+
import { SocialClient } from '@classytic/social';
|
|
107
|
+
|
|
108
|
+
const client = SocialClient.create({ /* ... */ });
|
|
109
|
+
|
|
110
|
+
export const tools = {
|
|
111
|
+
twitter_post: tool({
|
|
112
|
+
description: 'Post a tweet (≤280 characters)',
|
|
113
|
+
inputSchema: z.object({
|
|
114
|
+
text: z.string().max(280),
|
|
115
|
+
replyToTweetId: z.string().optional(),
|
|
116
|
+
}),
|
|
117
|
+
execute: ({ text, replyToTweetId }) => client.twitter!.post({
|
|
118
|
+
text,
|
|
119
|
+
perPlatform: { twitter: { replyTo: replyToTweetId } },
|
|
120
|
+
}),
|
|
121
|
+
}),
|
|
122
|
+
telegram_send: tool({
|
|
123
|
+
description: 'Send a message via the configured Telegram bot',
|
|
124
|
+
inputSchema: z.object({ text: z.string().min(1).max(4096) }),
|
|
125
|
+
execute: ({ text }) => client.telegram!.send(text),
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### OpenAI / Anthropic / MCP
|
|
131
|
+
|
|
132
|
+
Generate JSON Schema from any zod schema with `z.toJSONSchema()` (zod v4 builtin):
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { z } from 'zod';
|
|
136
|
+
|
|
137
|
+
const twitterPostInput = z.object({
|
|
138
|
+
text: z.string().max(280),
|
|
139
|
+
replyToTweetId: z.string().optional(),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// OpenAI Chat Completions
|
|
143
|
+
const openaiTool = {
|
|
144
|
+
type: 'function' as const,
|
|
145
|
+
function: {
|
|
146
|
+
name: 'twitter_post',
|
|
147
|
+
description: 'Post a tweet',
|
|
148
|
+
parameters: z.toJSONSchema(twitterPostInput),
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Anthropic Messages
|
|
153
|
+
const anthropicTool = {
|
|
154
|
+
name: 'twitter_post',
|
|
155
|
+
description: 'Post a tweet',
|
|
156
|
+
input_schema: z.toJSONSchema(twitterPostInput),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// MCP `tools/list`
|
|
160
|
+
const mcpTool = {
|
|
161
|
+
name: 'twitter_post',
|
|
162
|
+
description: 'Post a tweet',
|
|
163
|
+
inputSchema: z.toJSONSchema(twitterPostInput),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Then dispatch a tool call to the bound client:
|
|
167
|
+
async function dispatch(name: string, input: unknown) {
|
|
168
|
+
switch (name) {
|
|
169
|
+
case 'twitter_post': {
|
|
170
|
+
const args = twitterPostInput.parse(input);
|
|
171
|
+
return client.twitter!.post({
|
|
172
|
+
text: args.text,
|
|
173
|
+
perPlatform: { twitter: { replyTo: args.replyToTweetId } },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// ... other tools
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
That's 5 lines per tool. The package gives you:
|
|
182
|
+
- **Methods** — `client.twitter!.post(...)`, `client.telegram!.send(...)` etc.
|
|
183
|
+
- **Schemas** — pre-built zod v4 schemas in `@classytic/social/schemas`
|
|
184
|
+
- **Capabilities** — `allCapabilities`, `providersWithCapability('messaging')` for filtering
|
|
185
|
+
|
|
186
|
+
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.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## OAuth flows
|
|
191
|
+
|
|
192
|
+
Each sub-client exposes the standard OAuth lifecycle:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// 1. Build the consent URL
|
|
196
|
+
const url = await client.twitter!.authUrl('csrf-state-token');
|
|
197
|
+
// → redirect user
|
|
198
|
+
|
|
199
|
+
// 2. Exchange callback code (PKCE handled internally)
|
|
200
|
+
const tokens = await client.twitter!.exchangeCode(code, 'csrf-state-token');
|
|
201
|
+
// tokens are stored on `client.twitter.config.tokens`
|
|
202
|
+
|
|
203
|
+
// 3. Refresh later
|
|
204
|
+
const fresh = await client.twitter!.refresh();
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Telegram and WhatsApp use static tokens — no OAuth required.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Subpath exports
|
|
212
|
+
|
|
213
|
+
Tree-shakeable entrypoints — import only what you need.
|
|
214
|
+
|
|
215
|
+
| Subpath | Purpose |
|
|
216
|
+
|---|---|
|
|
217
|
+
| `@classytic/social` | `SocialClient`, `fromEnv`, all provider classes, errors |
|
|
218
|
+
| `@classytic/social/client` | Just the unified client + sub-clients |
|
|
219
|
+
| `@classytic/social/schemas` | Zod v4 schemas + capability map (for arc, OpenAPI, MCP) |
|
|
220
|
+
| `@classytic/social/common` | `httpRequest`, `oauth2*`, `paginate`, `pollUntilComplete`, `assertPublicHttpUrl` |
|
|
221
|
+
| `@classytic/social/<provider>` | Direct access to a single provider class |
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Schemas — design APIs from the SDK
|
|
226
|
+
|
|
227
|
+
Every credential, request input, and capability is a zod v4 schema. Generate JSON Schema, build forms, or validate at request boundaries:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { z } from 'zod';
|
|
231
|
+
import {
|
|
232
|
+
YouTubeUploadParamsSchema,
|
|
233
|
+
TelegramSendMessageSchema,
|
|
234
|
+
allCapabilities,
|
|
235
|
+
providersWithCapability,
|
|
236
|
+
} from '@classytic/social/schemas';
|
|
237
|
+
|
|
238
|
+
// Validate input
|
|
239
|
+
const params = YouTubeUploadParamsSchema.parse(body);
|
|
240
|
+
|
|
241
|
+
// Generate OpenAPI / MCP tool spec
|
|
242
|
+
const jsonSchema = z.toJSONSchema(YouTubeUploadParamsSchema);
|
|
243
|
+
|
|
244
|
+
// Filter providers by feature
|
|
245
|
+
providersWithCapability('scheduling'); // → ['youtube', 'tiktok', 'facebook']
|
|
246
|
+
providersWithCapability('messaging'); // → ['telegram', 'whatsapp']
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Capabilities matrix
|
|
252
|
+
|
|
253
|
+
| Provider | Auth | Posting | Upload | Messaging | Schedule | Delete | Analytics |
|
|
254
|
+
|---|---|---|---|---|---|---|---|
|
|
255
|
+
| YouTube | OAuth2 | — | ✓ | — | ✓ | ✓ | — |
|
|
256
|
+
| TikTok | OAuth2 + PKCE | ✓ | ✓ | — | ✓ | — | — |
|
|
257
|
+
| Instagram | OAuth2 (Meta) | ✓ | ✓ | — | — | — | ✓ |
|
|
258
|
+
| Facebook | OAuth2 (Meta) | ✓ | ✓ | — | ✓ | ✓ | ✓ |
|
|
259
|
+
| LinkedIn | OAuth2 | ✓ | ✓ | — | — | ✓ | ✓ |
|
|
260
|
+
| Twitter/X | OAuth2 + PKCE | ✓ | ✓ | ✓ (DM) | — | ✓ | — |
|
|
261
|
+
| Reddit | OAuth2 | ✓ | — | — | — | ✓ | — |
|
|
262
|
+
| Telegram | Bot Token | — | ✓ | ✓ | — | ✓ | — |
|
|
263
|
+
| WhatsApp | API Token | — | — | ✓ | — | — | — |
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Error handling
|
|
268
|
+
|
|
269
|
+
All providers throw `SocialError` with a uniform shape:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import { SocialError } from '@classytic/social';
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
await client.twitter!.post({ text: 'Hi' });
|
|
276
|
+
} catch (e) {
|
|
277
|
+
if (e instanceof SocialError) {
|
|
278
|
+
console.log(e.provider); // 'twitter'
|
|
279
|
+
console.log(e.statusCode); // 401, 429, 502, …
|
|
280
|
+
console.log(e.errorCode); // platform-specific code
|
|
281
|
+
console.log(e.retryable); // boolean | null
|
|
282
|
+
console.log(e.retryAfter); // seconds (for 429)
|
|
283
|
+
console.log(e.hint); // human-readable mitigation hint
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The unified `client.post()` / `client.upload()` never throw on per-platform failures — each result has `ok: boolean` and `error?` populated independently.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Custom HTTP / OAuth — build your own provider
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { httpRequest, oauth2ExchangeCode, paginate } from '@classytic/social/common';
|
|
296
|
+
|
|
297
|
+
// Same retry / timeout / 429 handling used internally
|
|
298
|
+
const { data } = await httpRequest('mastodon', {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
url: 'https://mastodon.social/api/v1/statuses',
|
|
301
|
+
bearer: accessToken,
|
|
302
|
+
json: { status: 'Hello' },
|
|
303
|
+
timeout: 30_000,
|
|
304
|
+
retry: { attempts: 2 },
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Environment variables (`fromEnv()`)
|
|
311
|
+
|
|
312
|
+
Set any subset — only matching platforms are enabled.
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# YouTube
|
|
316
|
+
YOUTUBE_CLIENT_ID=... YOUTUBE_CLIENT_SECRET=...
|
|
317
|
+
YOUTUBE_ACCESS_TOKEN=... YOUTUBE_REFRESH_TOKEN=...
|
|
318
|
+
|
|
319
|
+
# Twitter
|
|
320
|
+
TWITTER_CLIENT_ID=... TWITTER_CLIENT_SECRET=...
|
|
321
|
+
TWITTER_ACCESS_TOKEN=... TWITTER_REFRESH_TOKEN=...
|
|
322
|
+
|
|
323
|
+
# Facebook (Pages)
|
|
324
|
+
FACEBOOK_APP_ID=... FACEBOOK_APP_SECRET=...
|
|
325
|
+
FACEBOOK_ACCESS_TOKEN=... FACEBOOK_PAGE_ID=...
|
|
326
|
+
FACEBOOK_PAGE_ACCESS_TOKEN=...
|
|
327
|
+
|
|
328
|
+
# Instagram
|
|
329
|
+
INSTAGRAM_APP_ID=... INSTAGRAM_APP_SECRET=...
|
|
330
|
+
INSTAGRAM_ACCESS_TOKEN=... INSTAGRAM_USER_ID=...
|
|
331
|
+
|
|
332
|
+
# LinkedIn
|
|
333
|
+
LINKEDIN_CLIENT_ID=... LINKEDIN_CLIENT_SECRET=...
|
|
334
|
+
LINKEDIN_ACCESS_TOKEN=... LINKEDIN_AUTHOR_URN=urn:li:person:...
|
|
335
|
+
|
|
336
|
+
# Reddit
|
|
337
|
+
REDDIT_CLIENT_ID=... REDDIT_CLIENT_SECRET=...
|
|
338
|
+
REDDIT_USER_AGENT="web:my-app:v1.0 (by /u/yourname)"
|
|
339
|
+
REDDIT_ACCESS_TOKEN=... REDDIT_REFRESH_TOKEN=...
|
|
340
|
+
|
|
341
|
+
# TikTok
|
|
342
|
+
TIKTOK_CLIENT_KEY=... TIKTOK_CLIENT_SECRET=...
|
|
343
|
+
TIKTOK_ACCESS_TOKEN=... TIKTOK_REFRESH_TOKEN=...
|
|
344
|
+
|
|
345
|
+
# Telegram (bot)
|
|
346
|
+
TELEGRAM_BOT_TOKEN=123:abc TELEGRAM_CHAT_ID=@my-channel
|
|
347
|
+
|
|
348
|
+
# WhatsApp Business
|
|
349
|
+
WHATSAPP_ACCESS_TOKEN=... WHATSAPP_BUSINESS_ACCOUNT_ID=...
|
|
350
|
+
WHATSAPP_PHONE_NUMBER_ID=...
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Development
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
npm run build # tsdown → dist/
|
|
359
|
+
npm run typecheck # tsc --noEmit
|
|
360
|
+
npm test # vitest (325 tests)
|
|
361
|
+
npm run test:watch # vitest watch mode
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## License
|
|
367
|
+
|
|
368
|
+
MIT
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
//#region src/base.ts
|
|
2
|
+
var PlatformProvider = class {
|
|
3
|
+
name;
|
|
4
|
+
displayName;
|
|
5
|
+
config;
|
|
6
|
+
authType;
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.name = "";
|
|
9
|
+
this.displayName = "";
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.authType = "oauth2";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get OAuth authorization URL
|
|
15
|
+
* @param state - State parameter for CSRF protection
|
|
16
|
+
* @param credData - Decrypted credential data (client ID, redirect URI, etc.)
|
|
17
|
+
* @returns Authorization URL
|
|
18
|
+
*/
|
|
19
|
+
getAuthUrl(state, credData, options) {
|
|
20
|
+
throw new Error(`${this.name}: getAuthUrl() not implemented`);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Exchange authorization code for access token
|
|
24
|
+
* @param code - Authorization code from OAuth callback
|
|
25
|
+
* @param credData - Decrypted credential data
|
|
26
|
+
* @returns Token data { access_token, refresh_token, expires_in, ... }
|
|
27
|
+
*/
|
|
28
|
+
async exchangeCode(code, credData, state) {
|
|
29
|
+
throw new Error(`${this.name}: exchangeCode() not implemented`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Refresh access token using refresh token
|
|
33
|
+
* @param refreshToken - Refresh token
|
|
34
|
+
* @param credData - Decrypted credential data
|
|
35
|
+
* @returns New token data
|
|
36
|
+
*/
|
|
37
|
+
async refreshToken(refreshToken, credData) {
|
|
38
|
+
throw new Error(`${this.name}: refreshToken() not implemented`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get account information using access token
|
|
42
|
+
* @param accessToken - Access token
|
|
43
|
+
* @param credData - Decrypted credential data
|
|
44
|
+
* @returns Account info { id, name, email, profileImage, ... }
|
|
45
|
+
*/
|
|
46
|
+
async getAccountInfo(accessToken, credData) {
|
|
47
|
+
throw new Error(`${this.name}: getAccountInfo() not implemented`);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Revoke an OAuth access token. Forces the provider to show a fresh consent
|
|
51
|
+
* screen on next authorization (ensures newly enabled scopes are granted).
|
|
52
|
+
* Default: no-op. Override in providers that support token revocation.
|
|
53
|
+
*/
|
|
54
|
+
async revokeToken(accessToken, credData) {}
|
|
55
|
+
/**
|
|
56
|
+
* Test credential validity
|
|
57
|
+
* @param credentialData - Decrypted credential data
|
|
58
|
+
* @returns Test result { status: 'OK' | 'Error', message, ... }
|
|
59
|
+
*/
|
|
60
|
+
async testCredential(credentialData) {
|
|
61
|
+
throw new Error(`${this.name}: testCredential() not implemented`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Upload video to platform.
|
|
65
|
+
* Each provider defines its own parameter type — the base accepts any shape.
|
|
66
|
+
* @param params - Platform-specific upload parameters
|
|
67
|
+
* @returns Upload result
|
|
68
|
+
*/
|
|
69
|
+
async uploadVideo(params) {
|
|
70
|
+
throw new Error(`${this.name}: uploadVideo() not implemented`);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Upload photo(s) to platform.
|
|
74
|
+
* Each provider defines its own parameter type.
|
|
75
|
+
* @param params - Platform-specific upload parameters
|
|
76
|
+
* @returns Upload result
|
|
77
|
+
*/
|
|
78
|
+
async uploadPhoto(params) {
|
|
79
|
+
throw new Error(`${this.name}: uploadPhoto() not implemented`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Upload carousel (multi-media) post.
|
|
83
|
+
* Each provider defines its own parameter type.
|
|
84
|
+
* @param params - Platform-specific carousel parameters
|
|
85
|
+
* @returns Upload result
|
|
86
|
+
*/
|
|
87
|
+
async uploadCarousel(params) {
|
|
88
|
+
throw new Error(`${this.name}: uploadCarousel() not implemented`);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Send a message via the platform (Telegram, WhatsApp)
|
|
92
|
+
* @param args - Platform-specific arguments
|
|
93
|
+
*/
|
|
94
|
+
async sendMessage(...args) {
|
|
95
|
+
throw new Error(`${this.name}: sendMessage() not implemented`);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Delete/unpublish a post from the platform
|
|
99
|
+
* @param args - Platform-specific arguments (token, postId, etc.)
|
|
100
|
+
*/
|
|
101
|
+
async deletePost(...args) {
|
|
102
|
+
throw new Error(`${this.name}: deletePost() not implemented`);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Delete a message (Telegram-specific)
|
|
106
|
+
* @param botToken - Bot token
|
|
107
|
+
* @param chatId - Chat ID
|
|
108
|
+
* @param messageId - Message ID
|
|
109
|
+
*/
|
|
110
|
+
async deleteMessage(botToken, chatId, messageId) {
|
|
111
|
+
throw new Error(`${this.name}: deleteMessage() not implemented`);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get credential schema for UI form rendering.
|
|
115
|
+
*
|
|
116
|
+
* Default implementation derives fields from the zod schema returned by
|
|
117
|
+
* `getCredentialZodSchema()`. Providers may override either method —
|
|
118
|
+
* overriding `getCredentialZodSchema()` is preferred so consumers also get
|
|
119
|
+
* runtime validation, JSON Schema generation, and TypeScript inference.
|
|
120
|
+
*/
|
|
121
|
+
getCredentialSchema() {
|
|
122
|
+
const zodSchema = this.getCredentialZodSchema();
|
|
123
|
+
if (!zodSchema) return [];
|
|
124
|
+
return deriveCredentialFields(zodSchema);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Return the zod schema for this provider's credentials. Providers should
|
|
128
|
+
* override this to enable runtime validation and form schema derivation.
|
|
129
|
+
*
|
|
130
|
+
* @returns A zod object schema, or `null` if the provider doesn't expose
|
|
131
|
+
* credentials via this interface yet.
|
|
132
|
+
*/
|
|
133
|
+
getCredentialZodSchema() {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get provider metadata for frontend display and dynamic form rendering.
|
|
138
|
+
* Subclasses should override to provide platform-specific details.
|
|
139
|
+
*
|
|
140
|
+
* @returns Provider metadata
|
|
141
|
+
*/
|
|
142
|
+
getMetadata() {
|
|
143
|
+
return {
|
|
144
|
+
name: this.name,
|
|
145
|
+
displayName: this.displayName,
|
|
146
|
+
authType: this.authType,
|
|
147
|
+
icon: this.name,
|
|
148
|
+
brandColor: null,
|
|
149
|
+
description: "",
|
|
150
|
+
scopes: [],
|
|
151
|
+
scopeDescriptions: {},
|
|
152
|
+
setupGuide: [],
|
|
153
|
+
supportsScheduling: false,
|
|
154
|
+
supportsEnvironment: false,
|
|
155
|
+
redirectUriPattern: `/api/oauth/${this.name}/callback`,
|
|
156
|
+
credentialSchema: this.getCredentialSchema()
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Validate credential data using the provider's zod schema if available,
|
|
161
|
+
* falling back to a presence check on the UI schema.
|
|
162
|
+
*/
|
|
163
|
+
validateCredentials(data) {
|
|
164
|
+
const zodSchema = this.getCredentialZodSchema();
|
|
165
|
+
if (zodSchema) {
|
|
166
|
+
const result = zodSchema.safeParse(data);
|
|
167
|
+
if (result.success) return {
|
|
168
|
+
valid: true,
|
|
169
|
+
errors: []
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
valid: false,
|
|
173
|
+
errors: result.error.issues.map((issue) => {
|
|
174
|
+
return `${issue.path.length ? issue.path.join(".") + ": " : ""}${issue.message}`;
|
|
175
|
+
})
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const schema = this.getCredentialSchema();
|
|
179
|
+
const errors = [];
|
|
180
|
+
for (const field of schema) if (field.required && !data[field.name]) errors.push(`${field.displayName || field.name} is required`);
|
|
181
|
+
return {
|
|
182
|
+
valid: errors.length === 0,
|
|
183
|
+
errors
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const SECRET_NAME = /token|secret|password|key|credential/i;
|
|
188
|
+
/**
|
|
189
|
+
* Derive UI form fields from a zod object schema. Heuristics:
|
|
190
|
+
* - secret-named fields → `password`
|
|
191
|
+
* - z.url() → `url`
|
|
192
|
+
* - z.enum([...]) → `select`
|
|
193
|
+
* - everything else → `text`
|
|
194
|
+
*/
|
|
195
|
+
function deriveCredentialFields(schema) {
|
|
196
|
+
const def = schema.def;
|
|
197
|
+
if (!def || def.type !== "object" || !def.shape) return [];
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const [name, fieldSchema] of Object.entries(def.shape)) {
|
|
200
|
+
const innerDef = unwrap(fieldSchema).def;
|
|
201
|
+
let type = "text";
|
|
202
|
+
let options;
|
|
203
|
+
if (SECRET_NAME.test(name)) type = "password";
|
|
204
|
+
else if (innerDef?.type === "url" || innerDef?.format === "url") type = "url";
|
|
205
|
+
else if (innerDef?.type === "enum" && innerDef.entries) {
|
|
206
|
+
type = "select";
|
|
207
|
+
options = Object.values(innerDef.entries).map((v) => ({
|
|
208
|
+
label: String(v),
|
|
209
|
+
value: String(v)
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
out.push({
|
|
213
|
+
name,
|
|
214
|
+
displayName: humanize(name),
|
|
215
|
+
type,
|
|
216
|
+
required: !isOptional(fieldSchema),
|
|
217
|
+
description: fieldSchema.description,
|
|
218
|
+
options
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
function unwrap(t) {
|
|
224
|
+
let cur = t;
|
|
225
|
+
for (let i = 0; i < 8; i++) {
|
|
226
|
+
const def = cur.def;
|
|
227
|
+
if (!def?.innerType) return cur;
|
|
228
|
+
if (def.type === "optional" || def.type === "nullable" || def.type === "default") {
|
|
229
|
+
cur = def.innerType;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
return cur;
|
|
233
|
+
}
|
|
234
|
+
return cur;
|
|
235
|
+
}
|
|
236
|
+
function isOptional(t) {
|
|
237
|
+
const def = t.def;
|
|
238
|
+
return def?.type === "optional" || def?.type === "default" || def?.type === "nullable";
|
|
239
|
+
}
|
|
240
|
+
function humanize(name) {
|
|
241
|
+
return name.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase()).replace(/\bId\b/g, "ID").replace(/\bUrl\b/g, "URL").trim();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
//#endregion
|
|
245
|
+
export { deriveCredentialFields as n, PlatformProvider as t };
|
|
246
|
+
//# sourceMappingURL=base-Bw7e52V8.mjs.map
|