@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.
Files changed (121) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +368 -0
  4. package/dist/base-Bw7e52V8.mjs +246 -0
  5. package/dist/base-Bw7e52V8.mjs.map +1 -0
  6. package/dist/base-DBtKFiSX.d.mts +226 -0
  7. package/dist/base-DBtKFiSX.d.mts.map +1 -0
  8. package/dist/chunk-DQk6qfdC.mjs +18 -0
  9. package/dist/client/index.d.mts +44 -0
  10. package/dist/client/index.d.mts.map +1 -0
  11. package/dist/client/index.mjs +154 -0
  12. package/dist/client/index.mjs.map +1 -0
  13. package/dist/common/index.d.mts +3 -0
  14. package/dist/common/index.mjs +7 -0
  15. package/dist/contracts-Cdwa4zlg.d.mts +121 -0
  16. package/dist/contracts-Cdwa4zlg.d.mts.map +1 -0
  17. package/dist/contracts-lCa069IK.mjs +221 -0
  18. package/dist/contracts-lCa069IK.mjs.map +1 -0
  19. package/dist/env-Bl0cwwjC.mjs +955 -0
  20. package/dist/env-Bl0cwwjC.mjs.map +1 -0
  21. package/dist/env-DxOZHf0p.d.mts +394 -0
  22. package/dist/env-DxOZHf0p.d.mts.map +1 -0
  23. package/dist/errors-Cm6LeKf7.mjs +32 -0
  24. package/dist/errors-Cm6LeKf7.mjs.map +1 -0
  25. package/dist/facebook-l_4CghaA.mjs +95 -0
  26. package/dist/facebook-l_4CghaA.mjs.map +1 -0
  27. package/dist/http-DpcLSR1M.mjs +197 -0
  28. package/dist/http-DpcLSR1M.mjs.map +1 -0
  29. package/dist/index.d.mts +42 -0
  30. package/dist/index.d.mts.map +1 -0
  31. package/dist/index.mjs +71 -0
  32. package/dist/index.mjs.map +1 -0
  33. package/dist/instagram-BGaeUFU2.mjs +90 -0
  34. package/dist/instagram-BGaeUFU2.mjs.map +1 -0
  35. package/dist/linkedin-70whtVKa.mjs +101 -0
  36. package/dist/linkedin-70whtVKa.mjs.map +1 -0
  37. package/dist/meta-D3vcJU1c.mjs +126 -0
  38. package/dist/meta-D3vcJU1c.mjs.map +1 -0
  39. package/dist/pkce-jq5II68b.mjs +72 -0
  40. package/dist/pkce-jq5II68b.mjs.map +1 -0
  41. package/dist/polling-DZ1apXtA.mjs +25 -0
  42. package/dist/polling-DZ1apXtA.mjs.map +1 -0
  43. package/dist/providers/facebook.d.mts +135 -0
  44. package/dist/providers/facebook.d.mts.map +1 -0
  45. package/dist/providers/facebook.mjs +450 -0
  46. package/dist/providers/facebook.mjs.map +1 -0
  47. package/dist/providers/instagram.d.mts +122 -0
  48. package/dist/providers/instagram.d.mts.map +1 -0
  49. package/dist/providers/instagram.mjs +496 -0
  50. package/dist/providers/instagram.mjs.map +1 -0
  51. package/dist/providers/linkedin.d.mts +145 -0
  52. package/dist/providers/linkedin.d.mts.map +1 -0
  53. package/dist/providers/linkedin.mjs +574 -0
  54. package/dist/providers/linkedin.mjs.map +1 -0
  55. package/dist/providers/reddit.d.mts +102 -0
  56. package/dist/providers/reddit.d.mts.map +1 -0
  57. package/dist/providers/reddit.mjs +657 -0
  58. package/dist/providers/reddit.mjs.map +1 -0
  59. package/dist/providers/telegram.d.mts +139 -0
  60. package/dist/providers/telegram.d.mts.map +1 -0
  61. package/dist/providers/telegram.mjs +517 -0
  62. package/dist/providers/telegram.mjs.map +1 -0
  63. package/dist/providers/tiktok.d.mts +116 -0
  64. package/dist/providers/tiktok.d.mts.map +1 -0
  65. package/dist/providers/tiktok.mjs +676 -0
  66. package/dist/providers/tiktok.mjs.map +1 -0
  67. package/dist/providers/twitter.d.mts +150 -0
  68. package/dist/providers/twitter.d.mts.map +1 -0
  69. package/dist/providers/twitter.mjs +628 -0
  70. package/dist/providers/twitter.mjs.map +1 -0
  71. package/dist/providers/whatsapp.d.mts +79 -0
  72. package/dist/providers/whatsapp.d.mts.map +1 -0
  73. package/dist/providers/whatsapp.mjs +376 -0
  74. package/dist/providers/whatsapp.mjs.map +1 -0
  75. package/dist/providers/youtube.d.mts +153 -0
  76. package/dist/providers/youtube.d.mts.map +1 -0
  77. package/dist/providers/youtube.mjs +902 -0
  78. package/dist/providers/youtube.mjs.map +1 -0
  79. package/dist/reddit-B10kS4Se.mjs +126 -0
  80. package/dist/reddit-B10kS4Se.mjs.map +1 -0
  81. package/dist/schemas/index.d.mts +819 -0
  82. package/dist/schemas/index.d.mts.map +1 -0
  83. package/dist/schemas/index.mjs +31 -0
  84. package/dist/schemas/index.mjs.map +1 -0
  85. package/dist/security-BXhfebWm.d.mts +338 -0
  86. package/dist/security-BXhfebWm.d.mts.map +1 -0
  87. package/dist/shared-Fvc6xQku.mjs +100 -0
  88. package/dist/shared-Fvc6xQku.mjs.map +1 -0
  89. package/dist/telegram-FaUHpZgB.mjs +107 -0
  90. package/dist/telegram-FaUHpZgB.mjs.map +1 -0
  91. package/dist/tiktok-B_bMk4G-.mjs +94 -0
  92. package/dist/tiktok-B_bMk4G-.mjs.map +1 -0
  93. package/dist/twitter-BC22zfuc.mjs +98 -0
  94. package/dist/twitter-BC22zfuc.mjs.map +1 -0
  95. package/dist/types-BFE4psYI.d.mts +102 -0
  96. package/dist/types-BFE4psYI.d.mts.map +1 -0
  97. package/dist/types-Bv27tcT0.d.mts +230 -0
  98. package/dist/types-Bv27tcT0.d.mts.map +1 -0
  99. package/dist/types-BwkKyqpi.d.mts +253 -0
  100. package/dist/types-BwkKyqpi.d.mts.map +1 -0
  101. package/dist/types-CJrHMDV9.mjs +27 -0
  102. package/dist/types-CJrHMDV9.mjs.map +1 -0
  103. package/dist/types-ClbVc2rc.d.mts +117 -0
  104. package/dist/types-ClbVc2rc.d.mts.map +1 -0
  105. package/dist/types-D91N16Ym.d.mts +242 -0
  106. package/dist/types-D91N16Ym.d.mts.map +1 -0
  107. package/dist/types-DfLp_ibQ.d.mts +178 -0
  108. package/dist/types-DfLp_ibQ.d.mts.map +1 -0
  109. package/dist/types-DfjDgEoJ.d.mts +88 -0
  110. package/dist/types-DfjDgEoJ.d.mts.map +1 -0
  111. package/dist/types-Dp5Z9VBr.mjs +23 -0
  112. package/dist/types-Dp5Z9VBr.mjs.map +1 -0
  113. package/dist/types-hriBJTsU.d.mts +129 -0
  114. package/dist/types-hriBJTsU.d.mts.map +1 -0
  115. package/dist/types-rn6UuLL8.d.mts +184 -0
  116. package/dist/types-rn6UuLL8.d.mts.map +1 -0
  117. package/dist/whatsapp-CFp7ryR4.mjs +101 -0
  118. package/dist/whatsapp-CFp7ryR4.mjs.map +1 -0
  119. package/dist/youtube-Bs0fdY7H.mjs +98 -0
  120. package/dist/youtube-Bs0fdY7H.mjs.map +1 -0
  121. 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
+ [![Node](https://img.shields.io/badge/node-%3E%3D20-green)](https://nodejs.org)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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