@happyvertical/social 0.74.8

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/AGENT.md ADDED
@@ -0,0 +1,33 @@
1
+ # @happyvertical/social
2
+
3
+ <!-- BEGIN AGENT:GENERATED -->
4
+ ## Purpose
5
+ Social platform adapters for publishing to YouTube, Threads, X, and Bluesky
6
+
7
+ ## Package Map
8
+ - Package: `@happyvertical/social`
9
+ - Hierarchy path: `@happyvertical/sdk > packages > social`
10
+ - Workspace position: `25 of 30` local packages
11
+ - Internal dependencies: `@happyvertical/logger`, `@happyvertical/utils`
12
+ - Internal dependents: none
13
+ - Knowledge graph files: `AGENT.md`, `metadata.json`, `ecosystem-manifest.json`
14
+
15
+ ## Build & Test
16
+ ```bash
17
+ pnpm --filter @happyvertical/social build
18
+ pnpm --filter @happyvertical/social test
19
+ pnpm --filter @happyvertical/social clean
20
+ ```
21
+
22
+ ## Agent Correction Loops
23
+ - If module resolution or export errors mention a workspace dependency, build the dependency first (`pnpm --filter @happyvertical/logger build`, `pnpm --filter @happyvertical/utils build`) and then rerun `pnpm --filter @happyvertical/social build`.
24
+ - If tests or exports fail after API, type, or bundle changes, run `pnpm --filter @happyvertical/social clean` followed by `pnpm --filter @happyvertical/social build` and `pnpm --filter @happyvertical/social test`.
25
+ - If failures span multiple packages or Turborepo ordering looks wrong, run `pnpm build` and `pnpm typecheck` from the repo root before retrying package-scoped commands.
26
+
27
+ ## Ecosystem Relationships
28
+ - Provides: Social platform adapters for publishing to YouTube, Threads, X, and Bluesky
29
+ - Implements: none
30
+ - Requires: @happyvertical/logger, @happyvertical/utils
31
+ - Stability: stable (Primary package surface is described as implemented and production-oriented.)
32
+ <!-- END AGENT:GENERATED -->
33
+
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,485 @@
1
+ ---
2
+ id: social
3
+ title: "@happyvertical/social: Social Platform Publishing"
4
+ sidebar_label: "@happyvertical/social"
5
+ sidebar_position: 11
6
+ ---
7
+
8
+ # @happyvertical/social
9
+
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
11
+
12
+ A unified interface for publishing content to social platforms in the HAVE SDK.
13
+
14
+ ## Overview
15
+
16
+ The `@happyvertical/social` package provides adapters for publishing links, text, images, and videos to major social platforms including YouTube, Facebook Pages, Threads, X (Twitter), and Bluesky. Each adapter implements a consistent interface, making it easy to publish to multiple platforms with the same code.
17
+
18
+ ## Features
19
+
20
+ - **Multi-Platform Support**: YouTube, Facebook Pages, Threads, X (Twitter), Bluesky
21
+ - **Unified Interface**: Consistent API across all platforms
22
+ - **OAuth Support**: Built-in OAuth 2.0 with PKCE for YouTube and OAuth 1.0a/OAuth 2.0 support for X
23
+ - **Media Publishing**: Support for link, text, image, and video content
24
+ - **Safety Modes**: Dry-run and non-public publish modes for testing without public posts
25
+ - **Cross-Posting**: Publish to multiple platforms simultaneously
26
+ - **Analytics**: Retrieve post engagement metrics
27
+ - **Platform Capabilities**: Query platform-specific limits and features
28
+ - **Type-Safe**: Full TypeScript support with comprehensive type definitions
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ # Install with bun (recommended)
34
+ bun add @happyvertical/social
35
+
36
+ # Or with npm
37
+ npm install @happyvertical/social
38
+
39
+ # Or with pnpm
40
+ pnpm add @happyvertical/social
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ### Basic Usage
46
+
47
+ ```typescript
48
+ import { getSocial } from '@happyvertical/social';
49
+
50
+ // YouTube
51
+ const youtube = await getSocial({
52
+ type: 'youtube',
53
+ clientId: process.env.YOUTUBE_CLIENT_ID!,
54
+ clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
55
+ accessToken: 'user-access-token',
56
+ refreshToken: 'user-refresh-token',
57
+ });
58
+
59
+ // Publish video
60
+ const result = await youtube.publishVideo({
61
+ file: fs.readFileSync('video.mp4'),
62
+ title: 'Breaking News from Bentley',
63
+ description: 'Latest updates from the town council meeting.',
64
+ tags: ['news', 'local', 'bentley'],
65
+ });
66
+
67
+ console.log(`Published: ${result.url}`);
68
+ ```
69
+
70
+ ### Multiple Platforms
71
+
72
+ ```typescript
73
+ import { getSocial, getSocialMulti, publishToAll } from '@happyvertical/social';
74
+
75
+ // Create adapters for multiple platforms
76
+ const adapters = await getSocialMulti([
77
+ {
78
+ type: 'youtube',
79
+ clientId: process.env.YOUTUBE_CLIENT_ID!,
80
+ clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
81
+ accessToken: 'youtube-token',
82
+ },
83
+ {
84
+ type: 'bluesky',
85
+ identifier: 'myhandle.bsky.social',
86
+ password: process.env.BLUESKY_APP_PASSWORD!,
87
+ },
88
+ {
89
+ type: 'x',
90
+ apiKey: process.env.X_API_KEY!,
91
+ apiSecret: process.env.X_API_SECRET!,
92
+ accessToken: process.env.X_ACCESS_TOKEN!,
93
+ accessSecret: process.env.X_ACCESS_SECRET!,
94
+ },
95
+ {
96
+ type: 'facebook',
97
+ pageId: process.env.FACEBOOK_PAGE_ID!,
98
+ accessToken: process.env.FACEBOOK_PAGE_ACCESS_TOKEN!,
99
+ },
100
+ ]);
101
+
102
+ // Publish a story link to all supported platforms at once
103
+ const results = await publishToAll(adapters, {
104
+ type: 'link',
105
+ text: 'Breaking news from Bentley!',
106
+ url: 'https://example.com/article',
107
+ tags: ['news', 'local'],
108
+ });
109
+
110
+ // Check results per platform
111
+ for (const [platform, result] of results) {
112
+ if (result.success) {
113
+ console.log(`${platform}: Success`);
114
+ } else {
115
+ console.log(`${platform}: Failed - ${result.error?.message}`);
116
+ }
117
+ }
118
+ ```
119
+
120
+ ### Safety Modes
121
+
122
+ Adapters default to public publishing for backward compatibility. Set `publishMode` when testing request shapes or creating non-public platform objects.
123
+
124
+ ```typescript
125
+ const youtube = await getSocial({
126
+ type: 'youtube',
127
+ clientId: process.env.YOUTUBE_CLIENT_ID!,
128
+ clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
129
+ accessToken: 'user-access-token',
130
+ publishMode: 'private_or_scheduled',
131
+ });
132
+
133
+ const result = await youtube.publishVideo({
134
+ file: videoBuffer,
135
+ title: 'Council update',
136
+ isShort: true,
137
+ });
138
+
139
+ console.log(result.status); // "staged"
140
+ ```
141
+
142
+ ## Platform Adapters
143
+
144
+ ### YouTube
145
+
146
+ ```typescript
147
+ const youtube = await getSocial({
148
+ type: 'youtube',
149
+ clientId: 'your-client-id',
150
+ clientSecret: 'your-client-secret',
151
+ accessToken: 'user-access-token',
152
+ refreshToken: 'user-refresh-token',
153
+ });
154
+
155
+ // Publish video (supports Shorts with 9:16 aspect ratio)
156
+ await youtube.publishVideo({
157
+ file: videoBuffer,
158
+ title: 'Video Title',
159
+ description: 'Video description with #hashtags',
160
+ tags: ['tag1', 'tag2'],
161
+ visibility: 'public', // 'public' | 'unlisted' | 'private'
162
+ scheduledAt: new Date('2025-02-01'), // Optional scheduling
163
+ thumbnail: thumbnailBuffer, // Custom thumbnail
164
+ });
165
+
166
+ // OAuth flow
167
+ const { url, state, codeVerifier } = youtube.getAuthorizationUrl({
168
+ redirectUri: 'https://yourapp.com/callback',
169
+ scopes: ['https://www.googleapis.com/auth/youtube.upload'],
170
+ });
171
+
172
+ // Exchange code for tokens
173
+ const tokens = await youtube.exchangeCode({
174
+ code: authorizationCode,
175
+ redirectUri: 'https://yourapp.com/callback',
176
+ codeVerifier,
177
+ });
178
+ ```
179
+
180
+ ### Bluesky
181
+
182
+ ```typescript
183
+ const bluesky = await getSocial({
184
+ type: 'bluesky',
185
+ identifier: 'myhandle.bsky.social', // or DID
186
+ password: 'app-password', // Use app password, not main password
187
+ pdsUrl: 'https://bsky.social', // Optional custom PDS
188
+ });
189
+
190
+ // Authenticate
191
+ await bluesky.authenticate();
192
+
193
+ // Publish text with link card
194
+ await bluesky.publishText({
195
+ text: 'Check out this article!',
196
+ linkUrl: 'https://example.com/article',
197
+ tags: ['news'],
198
+ });
199
+
200
+ // Publish image
201
+ await bluesky.publishImage({
202
+ file: imageBuffer,
203
+ description: 'Image description',
204
+ altText: 'Accessible alt text',
205
+ });
206
+
207
+ // Publish a first-class link card
208
+ await bluesky.publishLink({
209
+ url: 'https://example.com/article',
210
+ title: 'Local story',
211
+ description: 'A short summary for the card',
212
+ });
213
+ ```
214
+
215
+ ### X (Twitter)
216
+
217
+ ```typescript
218
+ const x = await getSocial({
219
+ type: 'x',
220
+ apiKey: 'consumer-key',
221
+ apiSecret: 'consumer-secret',
222
+ accessToken: 'user-access-token',
223
+ accessSecret: 'user-access-secret',
224
+ });
225
+
226
+ // Authenticate
227
+ await x.authenticate();
228
+
229
+ // Publish text
230
+ await x.publishText({
231
+ text: 'Hello from Bentley! 🏔️',
232
+ tags: ['news', 'local'],
233
+ });
234
+
235
+ // Publish with image
236
+ await x.publishImage({
237
+ file: imageBuffer,
238
+ description: 'Breaking news',
239
+ altText: 'News headline image',
240
+ });
241
+
242
+ // Publish video
243
+ await x.publishVideo({
244
+ file: videoBuffer,
245
+ description: 'Watch the latest update',
246
+ linkUrl: 'https://example.com', // Inline by default
247
+ linkBehavior: 'reply', // Optional: post the link as a reply instead
248
+ });
249
+ ```
250
+
251
+ ### Threads
252
+
253
+ ```typescript
254
+ const threads = await getSocial({
255
+ type: 'threads',
256
+ accessToken: 'meta-access-token',
257
+ userId: 'threads-user-id',
258
+ });
259
+
260
+ // Publish text
261
+ await threads.publishText({
262
+ text: 'Hello from Threads!',
263
+ tags: ['meta', 'social'],
264
+ });
265
+
266
+ // Publish image (requires publicly accessible URL)
267
+ await threads.publishImage({
268
+ file: 'https://example.com/image.png', // URL required, not buffer
269
+ description: 'Image caption',
270
+ });
271
+
272
+ // Publish a link attachment
273
+ await threads.publishLink({
274
+ url: 'https://example.com/article',
275
+ text: 'Read the latest story',
276
+ });
277
+ ```
278
+
279
+ ### Facebook Pages
280
+
281
+ ```typescript
282
+ const facebook = await getSocial({
283
+ type: 'facebook',
284
+ pageId: 'page-id',
285
+ accessToken: 'page-access-token',
286
+ });
287
+
288
+ // Publish a Page feed link post
289
+ await facebook.publishLink({
290
+ url: 'https://example.com/article',
291
+ text: 'Read the latest story',
292
+ });
293
+
294
+ // Create an unpublished Page post for testing
295
+ const safeFacebook = await getSocial({
296
+ type: 'facebook',
297
+ pageId: 'page-id',
298
+ accessToken: 'page-access-token',
299
+ publishMode: 'private_or_scheduled',
300
+ });
301
+
302
+ await safeFacebook.publishText({
303
+ text: 'Draft post',
304
+ });
305
+ ```
306
+
307
+ ## API Reference
308
+
309
+ ### SocialPlatform Interface
310
+
311
+ All adapters implement this interface:
312
+
313
+ ```typescript
314
+ interface SocialPlatform {
315
+ readonly platform: string;
316
+
317
+ // Authentication
318
+ authenticate(): Promise<AuthResult>;
319
+ refreshToken(token: string): Promise<AuthResult>;
320
+
321
+ // Publishing
322
+ publishVideo(video: VideoPost): Promise<PostResult>;
323
+ publishImage(image: ImagePost): Promise<PostResult>;
324
+ publishText(text: TextPost): Promise<PostResult>;
325
+ publishLink(link: LinkPost): Promise<PostResult>;
326
+
327
+ // Management
328
+ getPost(postId: string): Promise<Post>;
329
+ deletePost(postId: string): Promise<void>;
330
+ getAnalytics(postId: string): Promise<PostAnalytics>;
331
+
332
+ // Capabilities
333
+ getCapabilities(): PlatformCapabilities;
334
+ }
335
+ ```
336
+
337
+ ### Platform Capabilities
338
+
339
+ ```typescript
340
+ const caps = youtube.getCapabilities();
341
+ console.log(`Max video length: ${caps.maxVideoLength}s`);
342
+ console.log(`Max video size: ${caps.maxVideoSize / (1024 * 1024)}MB`);
343
+ console.log(`Supports scheduling: ${caps.scheduling}`);
344
+ ```
345
+
346
+ | Platform | Link | Video | Image | Text | Scheduling | Safe non-public mode | Max Video |
347
+ |----------|------|-------|-------|------|------------|----------------------|-----------|
348
+ | YouTube | ✗ | ✓ | ✗ | ✗ | ✓ | private upload | 256GB |
349
+ | Facebook Pages | ✓ | ✓ | ✓ | ✓ | ✓ | unpublished/scheduled | 10GB |
350
+ | Threads | ✓ | ✓ | ✓ | ✓ | ✗ | staged container | 1GB |
351
+ | X | ✓ | ✓ | ✓ | ✓ | ✗ | staged media/dry run | 512MB |
352
+ | Bluesky | ✓ | ✗ | ✓ | ✓ | ✗ | dry run/blob staging | N/A |
353
+
354
+ ## Error Handling
355
+
356
+ ```typescript
357
+ import {
358
+ getSocial,
359
+ SocialError,
360
+ SocialAuthError,
361
+ SocialRateLimitError,
362
+ } from '@happyvertical/social';
363
+
364
+ try {
365
+ await adapter.publishText({ text: 'Hello!' });
366
+ } catch (error) {
367
+ if (error instanceof SocialAuthError) {
368
+ console.error(`Auth error on ${error.platform}: ${error.message}`);
369
+ // Refresh token or re-authenticate
370
+ } else if (error instanceof SocialRateLimitError) {
371
+ console.error(`Rate limited. Retry after ${error.retryAfter}s`);
372
+ } else if (error instanceof SocialError) {
373
+ console.error(`Error: ${error.code} - ${error.message}`);
374
+ }
375
+ }
376
+ ```
377
+
378
+ ## Types
379
+
380
+ ```typescript
381
+ interface VideoPost {
382
+ file: Buffer | string;
383
+ title?: string;
384
+ description?: string;
385
+ thumbnail?: Buffer | string;
386
+ tags?: string[];
387
+ linkUrl?: string;
388
+ visibility?: 'public' | 'unlisted' | 'private';
389
+ scheduledAt?: Date;
390
+ categoryId?: string; // YouTube category
391
+ isShort?: boolean;
392
+ linkBehavior?: 'inline' | 'attachment' | 'reply' | 'none';
393
+ }
394
+
395
+ interface ImagePost {
396
+ file: Buffer | string;
397
+ description?: string;
398
+ altText?: string;
399
+ linkUrl?: string;
400
+ tags?: string[];
401
+ linkBehavior?: 'inline' | 'attachment' | 'reply' | 'none';
402
+ }
403
+
404
+ interface TextPost {
405
+ text: string;
406
+ linkUrl?: string;
407
+ tags?: string[];
408
+ replyTo?: string; // Post ID to reply to
409
+ linkBehavior?: 'inline' | 'attachment' | 'reply' | 'none';
410
+ }
411
+
412
+ interface LinkPost {
413
+ url: string;
414
+ text?: string;
415
+ title?: string;
416
+ description?: string;
417
+ tags?: string[];
418
+ scheduledAt?: Date;
419
+ linkBehavior?: 'inline' | 'attachment' | 'reply' | 'none';
420
+ }
421
+
422
+ interface PostResult {
423
+ id: string;
424
+ url: string;
425
+ status: 'published' | 'scheduled' | 'processing' | 'staged' | 'dry_run';
426
+ publishedAt?: Date;
427
+ scheduledAt?: Date;
428
+ }
429
+
430
+ interface PostAnalytics {
431
+ views?: number;
432
+ impressions?: number;
433
+ likes?: number;
434
+ comments?: number;
435
+ shares?: number;
436
+ clicks?: number;
437
+ raw?: unknown;
438
+ lastUpdated?: Date;
439
+ }
440
+ ```
441
+
442
+ ## Best Practices
443
+
444
+ ### Credential Management
445
+
446
+ ```typescript
447
+ // Use environment variables
448
+ const youtube = await getSocial({
449
+ type: 'youtube',
450
+ clientId: process.env.YOUTUBE_CLIENT_ID!,
451
+ clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
452
+ accessToken: await getStoredToken('youtube'),
453
+ });
454
+
455
+ // Implement token refresh
456
+ youtube.authenticate().catch(async (error) => {
457
+ if (error instanceof SocialAuthError) {
458
+ const refreshed = await youtube.refreshToken(storedRefreshToken);
459
+ await storeToken('youtube', refreshed.accessToken);
460
+ }
461
+ });
462
+ ```
463
+
464
+ ### Platform-Specific Optimization
465
+
466
+ ```typescript
467
+ // X: Inline links by default, or post links as replies per account/post
468
+ await x.publishVideo({
469
+ file: videoBuffer,
470
+ description: 'Watch the news',
471
+ linkUrl: 'https://example.com/article',
472
+ linkBehavior: 'reply',
473
+ });
474
+
475
+ // YouTube: Use scheduling for optimal posting times
476
+ await youtube.publishVideo({
477
+ file: videoBuffer,
478
+ title: 'Morning News',
479
+ scheduledAt: new Date('2025-01-27T08:00:00Z'),
480
+ });
481
+ ```
482
+
483
+ ## License
484
+
485
+ This package is part of the HAVE SDK and is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.