@nextsparkjs/plugin-social-media-publisher 0.1.0-beta.1

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.
@@ -0,0 +1,527 @@
1
+ # Publishing
2
+
3
+ ## Overview
4
+
5
+ The Social Media Publisher plugin provides a unified endpoint for publishing content to Instagram Business and Facebook Pages. Publishing includes automatic token validation, token refresh if needed, and comprehensive audit logging.
6
+
7
+ **Endpoint:** `POST /api/v1/plugin/social-media-publisher/social/publish`
8
+
9
+ **Supported Platforms:**
10
+ - Instagram Business (photos and videos)
11
+ - Facebook Pages (text, photos, and links)
12
+
13
+ ## Publish Endpoint
14
+
15
+ ### Request Schema
16
+
17
+ ```typescript
18
+ {
19
+ accountId: string // UUID of social platform account
20
+ platform: string // 'instagram_business' | 'facebook_page'
21
+ imageUrl?: string // Public HTTPS URL to image
22
+ videoUrl?: string // Public HTTPS URL to video
23
+ caption?: string // Post caption/message
24
+ link?: string // Link URL (Facebook only)
25
+ }
26
+ ```
27
+
28
+ ### Response Schema
29
+
30
+ **Success:**
31
+ ```json
32
+ {
33
+ "success": true,
34
+ "platform": "instagram_business",
35
+ "postId": "17899618652010220",
36
+ "postUrl": "https://www.instagram.com/p/ABC123",
37
+ "message": "Successfully published to instagram_business"
38
+ }
39
+ ```
40
+
41
+ **Error:**
42
+ ```json
43
+ {
44
+ "error": "Publishing failed",
45
+ "platform": "instagram_business",
46
+ "details": "Image URL must be publicly accessible",
47
+ "errorDetails": {
48
+ "code": 100,
49
+ "message": "Invalid parameter"
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Instagram Business Publishing
55
+
56
+ ### Photo Posts
57
+
58
+ Instagram uses a **2-step container process**:
59
+
60
+ 1. **Create Media Container** - Upload image URL and caption
61
+ 2. **Publish Container** - Make post live on profile
62
+
63
+ **Request:**
64
+ ```typescript
65
+ {
66
+ accountId: "550e8400-e29b-41d4-a716-446655440000",
67
+ platform: "instagram_business",
68
+ imageUrl: "https://example.com/image.jpg",
69
+ caption: "Check out our new product! 🚀 #launch"
70
+ }
71
+ ```
72
+
73
+ **Process Flow:**
74
+ ```
75
+ 1. Validate request
76
+ 2. Get account from database (via RLS)
77
+ 3. Check token expiration
78
+ 4. Auto-refresh if < 10 minutes until expiry
79
+ 5. Decrypt access token
80
+ 6. Create media container (Instagram API)
81
+ 7. Wait 2 seconds (Instagram processing)
82
+ 8. Publish container (Instagram API)
83
+ 9. Create audit log
84
+ 10. Return post URL
85
+ ```
86
+
87
+ **Code Example:**
88
+ ```typescript
89
+ // Using fetch API
90
+ const response = await fetch('/api/v1/plugin/social-media-publisher/social/publish', {
91
+ method: 'POST',
92
+ headers: {
93
+ 'Content-Type': 'application/json',
94
+ },
95
+ body: JSON.stringify({
96
+ accountId: 'account-uuid-here',
97
+ platform: 'instagram_business',
98
+ imageUrl: 'https://cdn.example.com/photo.jpg',
99
+ caption: 'My awesome post! #instagram'
100
+ })
101
+ })
102
+
103
+ const result = await response.json()
104
+
105
+ if (result.success) {
106
+ console.log('Posted to Instagram:', result.postUrl)
107
+ } else {
108
+ console.error('Failed:', result.error)
109
+ }
110
+ ```
111
+
112
+ **Image Requirements:**
113
+ - **Format:** JPG or PNG
114
+ - **Size:** Max 8MB
115
+ - **Dimensions:** 320px to 1080px (width)
116
+ - **Aspect Ratio:** 4:5 to 1.91:1
117
+ - **Accessibility:** Must be publicly accessible via HTTPS
118
+ - **No Redirects:** Direct image URL only
119
+
120
+ **Caption Requirements:**
121
+ - **Length:** Max 2,200 characters
122
+ - **Hashtags:** Up to 30 per post
123
+ - **Mentions:** Up to 20 per post (@username)
124
+ - **Emojis:** Supported ✅
125
+ - **Line Breaks:** Supported (`\n`)
126
+
127
+ ### Video Posts
128
+
129
+ Similar to photos but with **longer processing time**:
130
+
131
+ **Request:**
132
+ ```typescript
133
+ {
134
+ accountId: "account-uuid",
135
+ platform: "instagram_business",
136
+ videoUrl: "https://example.com/video.mp4",
137
+ caption: "Check out this video! 🎬"
138
+ }
139
+ ```
140
+
141
+ **Process Flow:**
142
+ ```
143
+ 1-5. Same as photo posts
144
+ 6. Create video container
145
+ 7. Poll for video processing (up to 30 seconds)
146
+ 8. Publish when status = 'FINISHED'
147
+ 9. Create audit log
148
+ 10. Return post URL
149
+ ```
150
+
151
+ **Video Requirements:**
152
+ - **Format:** MP4 or MOV
153
+ - **Size:** Max 100MB
154
+ - **Duration:** 3 seconds to 60 seconds
155
+ - **Dimensions:** Min 600px (any dimension)
156
+ - **Aspect Ratio:** 4:5 to 1.91:1
157
+ - **Frame Rate:** Max 30fps
158
+ - **Accessibility:** Must be publicly accessible via HTTPS
159
+
160
+ **Processing Time:**
161
+ - Small videos (< 10MB): 5-10 seconds
162
+ - Medium videos (10-50MB): 15-30 seconds
163
+ - Large videos (50-100MB): 30-60 seconds
164
+
165
+ ## Facebook Pages Publishing
166
+
167
+ ### Text Posts
168
+
169
+ **Request:**
170
+ ```typescript
171
+ {
172
+ accountId: "account-uuid",
173
+ platform: "facebook_page",
174
+ caption: "Just a text post on Facebook!"
175
+ }
176
+ ```
177
+
178
+ **Process:**
179
+ ```
180
+ POST https://graph.facebook.com/v18.0/{PAGE_ID}/feed
181
+ {
182
+ message: "Post text here",
183
+ access_token: "{PAGE_TOKEN}"
184
+ }
185
+ ```
186
+
187
+ ### Photo Posts
188
+
189
+ **Request:**
190
+ ```typescript
191
+ {
192
+ accountId: "account-uuid",
193
+ platform: "facebook_page",
194
+ imageUrl: "https://example.com/image.jpg",
195
+ caption: "Check out this photo!"
196
+ }
197
+ ```
198
+
199
+ **Process:**
200
+ ```
201
+ POST https://graph.facebook.com/v18.0/{PAGE_ID}/photos
202
+ {
203
+ url: "https://example.com/image.jpg",
204
+ message: "Photo caption",
205
+ access_token: "{PAGE_TOKEN}"
206
+ }
207
+ ```
208
+
209
+ **Image Requirements:**
210
+ - **Format:** JPG, PNG, GIF, BMP
211
+ - **Size:** Max 4MB (recommended), up to 15MB
212
+ - **Dimensions:** No strict limits, but 1200x630px recommended
213
+ - **Accessibility:** Must be publicly accessible
214
+
215
+ ### Link Posts
216
+
217
+ **Request:**
218
+ ```typescript
219
+ {
220
+ accountId: "account-uuid",
221
+ platform: "facebook_page",
222
+ link: "https://example.com/article",
223
+ caption: "Read our latest article!"
224
+ }
225
+ ```
226
+
227
+ **Process:**
228
+ ```
229
+ POST https://graph.facebook.com/v18.0/{PAGE_ID}/feed
230
+ {
231
+ message: "Check this out!",
232
+ link: "https://example.com/article",
233
+ access_token: "{PAGE_TOKEN}"
234
+ }
235
+ ```
236
+
237
+ **Link Requirements:**
238
+ - **Protocol:** Must be HTTPS (HTTP not allowed)
239
+ - **Preview:** Facebook auto-generates preview (Open Graph)
240
+ - **Meta Tags:** Use Open Graph tags for better preview
241
+
242
+ ## Validation
243
+
244
+ ### Image URL Validation
245
+
246
+ ```typescript
247
+ function validateImageUrl(url: string): { valid: boolean; error?: string } {
248
+ // Must be HTTPS
249
+ if (!url.startsWith('https://')) {
250
+ return { valid: false, error: 'Image URL must use HTTPS' }
251
+ }
252
+
253
+ // Must end with image extension
254
+ const validExtensions = ['.jpg', '.jpeg', '.png', '.gif']
255
+ const hasValidExtension = validExtensions.some(ext =>
256
+ url.toLowerCase().includes(ext)
257
+ )
258
+
259
+ if (!hasValidExtension) {
260
+ return { valid: false, error: 'URL must point to an image file' }
261
+ }
262
+
263
+ // Must be publicly accessible (checked at runtime by platform)
264
+ return { valid: true }
265
+ }
266
+ ```
267
+
268
+ ### Caption Validation
269
+
270
+ ```typescript
271
+ function validateCaption(
272
+ caption: string,
273
+ platform: 'instagram_business' | 'facebook_page'
274
+ ): { valid: boolean; error?: string } {
275
+ // Instagram: Max 2,200 characters
276
+ if (platform === 'instagram_business' && caption.length > 2200) {
277
+ return {
278
+ valid: false,
279
+ error: 'Instagram captions must be under 2,200 characters'
280
+ }
281
+ }
282
+
283
+ // Facebook: Max 63,206 characters
284
+ if (platform === 'facebook_page' && caption.length > 63206) {
285
+ return {
286
+ valid: false,
287
+ error: 'Facebook posts must be under 63,206 characters'
288
+ }
289
+ }
290
+
291
+ return { valid: true }
292
+ }
293
+ ```
294
+
295
+ ## Automatic Token Refresh
296
+
297
+ Before every publish operation, the plugin checks if the token expires soon and automatically refreshes it if needed.
298
+
299
+ **Refresh Logic:**
300
+ ```typescript
301
+ const now = new Date()
302
+ const expiresAt = new Date(account.tokenExpiresAt)
303
+ const minutesUntilExpiry = (expiresAt.getTime() - now.getTime()) / (1000 * 60)
304
+
305
+ // Refresh if < 10 minutes until expiration
306
+ if (minutesUntilExpiry < 10) {
307
+ const refreshResult = await refreshAccountToken(accountId, platform, decryptedToken)
308
+
309
+ if (refreshResult.success) {
310
+ // Use refreshed token for this request
311
+ decryptedToken = refreshResult.newAccessToken
312
+ } else {
313
+ // Fail publish operation
314
+ return error('Token expired and refresh failed')
315
+ }
316
+ }
317
+ ```
318
+
319
+ **Benefits:**
320
+ - ✅ No publish failures due to expired tokens
321
+ - ✅ Transparent to users
322
+ - ✅ Automatic for all accounts
323
+ - ✅ Logged in audit trail
324
+
325
+ ## Usage Examples
326
+
327
+ ### React Component
328
+
329
+ ```typescript
330
+ 'use client'
331
+
332
+ import { useState } from 'react'
333
+
334
+ export function PublishForm({ accountId, platform }: {
335
+ accountId: string
336
+ platform: 'instagram_business' | 'facebook_page'
337
+ }) {
338
+ const [imageUrl, setImageUrl] = useState('')
339
+ const [caption, setCaption] = useState('')
340
+ const [loading, setLoading] = useState(false)
341
+ const [result, setResult] = useState<any>(null)
342
+
343
+ const handlePublish = async () => {
344
+ setLoading(true)
345
+ setResult(null)
346
+
347
+ try {
348
+ const response = await fetch('/api/v1/plugin/social-media-publisher/social/publish', {
349
+ method: 'POST',
350
+ headers: { 'Content-Type': 'application/json' },
351
+ body: JSON.stringify({
352
+ accountId,
353
+ platform,
354
+ imageUrl,
355
+ caption
356
+ })
357
+ })
358
+
359
+ const data = await response.json()
360
+ setResult(data)
361
+
362
+ if (data.success) {
363
+ alert(`Published successfully! View at: ${data.postUrl}`)
364
+ }
365
+ } catch (error) {
366
+ setResult({ error: 'Network error' })
367
+ } finally {
368
+ setLoading(false)
369
+ }
370
+ }
371
+
372
+ return (
373
+ <div>
374
+ <input
375
+ type="url"
376
+ placeholder="Image URL (https://...)"
377
+ value={imageUrl}
378
+ onChange={(e) => setImageUrl(e.target.value)}
379
+ />
380
+
381
+ <textarea
382
+ placeholder="Caption..."
383
+ value={caption}
384
+ onChange={(e) => setCaption(e.target.value)}
385
+ maxLength={platform === 'instagram_business' ? 2200 : 63206}
386
+ />
387
+
388
+ <button
389
+ onClick={handlePublish}
390
+ disabled={loading || !imageUrl}
391
+ >
392
+ {loading ? 'Publishing...' : 'Publish'}
393
+ </button>
394
+
395
+ {result && (
396
+ <div>
397
+ {result.success ? (
398
+ <a href={result.postUrl} target="_blank">
399
+ View Post →
400
+ </a>
401
+ ) : (
402
+ <p>Error: {result.error}</p>
403
+ )}
404
+ </div>
405
+ )}
406
+ </div>
407
+ )
408
+ }
409
+ ```
410
+
411
+ ### Batch Publishing
412
+
413
+ Publish to multiple accounts:
414
+
415
+ ```typescript
416
+ async function publishToMultipleAccounts(
417
+ accounts: Array<{ id: string; platform: string }>,
418
+ imageUrl: string,
419
+ caption: string
420
+ ) {
421
+ const results = await Promise.allSettled(
422
+ accounts.map(account =>
423
+ fetch('/api/v1/plugin/social-media-publisher/social/publish', {
424
+ method: 'POST',
425
+ headers: { 'Content-Type': 'application/json' },
426
+ body: JSON.stringify({
427
+ accountId: account.id,
428
+ platform: account.platform,
429
+ imageUrl,
430
+ caption
431
+ })
432
+ }).then(r => r.json())
433
+ )
434
+ )
435
+
436
+ const successful = results.filter(r => r.status === 'fulfilled')
437
+ const failed = results.filter(r => r.status === 'rejected')
438
+
439
+ return {
440
+ total: accounts.length,
441
+ successful: successful.length,
442
+ failed: failed.length,
443
+ results
444
+ }
445
+ }
446
+ ```
447
+
448
+ ## Error Handling
449
+
450
+ ### Common Errors
451
+
452
+ **1. Token Expired**
453
+ ```json
454
+ {
455
+ "error": "Token expired and refresh failed",
456
+ "details": "Meta API error: Invalid OAuth 2.0 Access Token",
457
+ "suggestion": "Please reconnect your social media account"
458
+ }
459
+ ```
460
+ **Solution:** User must reconnect account via OAuth
461
+
462
+ **2. Image Not Accessible**
463
+ ```json
464
+ {
465
+ "error": "Publishing failed",
466
+ "details": "Image URL must be publicly accessible",
467
+ "errorDetails": {
468
+ "code": 100,
469
+ "message": "Invalid parameter"
470
+ }
471
+ }
472
+ ```
473
+ **Solution:** Ensure image URL is publicly accessible via HTTPS
474
+
475
+ **3. Rate Limit**
476
+ ```json
477
+ {
478
+ "error": "Publishing failed",
479
+ "details": "Application request limit reached",
480
+ "errorDetails": {
481
+ "code": 4,
482
+ "message": "Application request limit reached"
483
+ }
484
+ }
485
+ ```
486
+ **Solution:** Wait and retry, or upgrade Facebook App tier
487
+
488
+ **4. Invalid Permissions**
489
+ ```json
490
+ {
491
+ "error": "Publishing failed",
492
+ "details": "(#200) Requires extended permission: instagram_content_publish"
493
+ }
494
+ ```
495
+ **Solution:** User must reconnect account and grant required permissions
496
+
497
+ ## Best Practices
498
+
499
+ ### Image Hosting
500
+
501
+ ✅ **Use CDN** for image hosting (CloudFlare, AWS CloudFront)
502
+ ✅ **HTTPS Only** - HTTP URLs rejected
503
+ ✅ **Direct URLs** - No redirects
504
+ ✅ **Optimize Size** - Compress images before upload
505
+ ✅ **Test Accessibility** - Verify URL is publicly accessible
506
+
507
+ ### Caption Writing
508
+
509
+ ✅ **Keep Under Limits** - 2,200 for Instagram, 63,206 for Facebook
510
+ ✅ **Use Hashtags** - Max 30 per Instagram post
511
+ ✅ **Test Emojis** - Ensure they render correctly
512
+ ✅ **Line Breaks** - Use `\n` for readability
513
+ ✅ **Call-to-Action** - Include clear CTA
514
+
515
+ ### Error Handling
516
+
517
+ ✅ **Show User-Friendly Messages** - Don't expose technical errors
518
+ ✅ **Retry Logic** - Implement exponential backoff for rate limits
519
+ ✅ **Token Refresh** - Handle automatic refresh transparently
520
+ ✅ **Audit Logging** - Log all attempts for debugging
521
+ ✅ **Validation** - Validate before sending to API
522
+
523
+ ## Next Steps
524
+
525
+ - **[Token Management](./03-token-management.md)** - Understand token lifecycle
526
+ - **[Audit Logging](./04-audit-logging.md)** - Track all publishing events
527
+ - **[Provider APIs](../03-advanced-usage/01-provider-apis.md)** - Use API wrappers directly