@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,646 @@
1
+ # Audit Logging
2
+
3
+ ## Overview
4
+
5
+ The Social Media Publisher plugin implements a comprehensive, **immutable audit trail** that logs every significant action performed through the system. This provides complete visibility for compliance, debugging, billing, and security monitoring.
6
+
7
+ **Key Characteristics:**
8
+ - **Immutable** - Logs cannot be modified or deleted
9
+ - **Complete** - Every action logged with full context
10
+ - **Attributed** - User, account, IP, and User-Agent tracking
11
+ - **Timestamped** - Precise timestamp for all events
12
+ - **GDPR-Compliant** - Personal data handling follows regulations
13
+
14
+ ## Audit Logs Entity
15
+
16
+ ### Database Schema
17
+
18
+ ```sql
19
+ CREATE TABLE "audit_logs" (
20
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
21
+ "userId" TEXT NOT NULL, -- Who performed the action
22
+ "accountId" UUID, -- Which social account (optional)
23
+ action TEXT NOT NULL, -- What happened
24
+ details JSONB DEFAULT '{}', -- Full context
25
+ "ipAddress" TEXT, -- IP address of request
26
+ "userAgent" TEXT, -- Browser/client info
27
+ "createdAt" TIMESTAMPTZ DEFAULT now() -- When it happened
28
+ );
29
+
30
+ -- Indexes for common queries
31
+ CREATE INDEX "idx_audit_logs_userId" ON "audit_logs"("userId");
32
+ CREATE INDEX "idx_audit_logs_accountId" ON "audit_logs"("accountId");
33
+ CREATE INDEX "idx_audit_logs_action" ON "audit_logs"(action);
34
+ CREATE INDEX "idx_audit_logs_createdAt" ON "audit_logs"("createdAt" DESC);
35
+ ```
36
+
37
+ ### Field Descriptions
38
+
39
+ **id** (UUID)
40
+ - Unique identifier for log entry
41
+ - Auto-generated
42
+ - Used for reference and deduplication
43
+
44
+ **userId** (TEXT)
45
+ - User who performed the action
46
+ - References `users.id`
47
+ - Never null (system user for automated actions)
48
+
49
+ **accountId** (UUID, nullable)
50
+ - Social media account involved
51
+ - References `clients_social_platforms.id`
52
+ - Null for actions not tied to specific account
53
+
54
+ **action** (TEXT)
55
+ - Type of action performed
56
+ - Standardized enum values
57
+ - Used for filtering and reporting
58
+
59
+ **details** (JSONB)
60
+ - Full context of the action
61
+ - Flexible structure
62
+ - Includes relevant metadata
63
+
64
+ **ipAddress** (TEXT, nullable)
65
+ - IP address of request origin
66
+ - Useful for security monitoring
67
+ - Null for system-initiated actions
68
+
69
+ **userAgent** (TEXT, nullable)
70
+ - Browser/client information
71
+ - Helps identify automation vs human
72
+ - Null for backend processes
73
+
74
+ **createdAt** (TIMESTAMPTZ)
75
+ - Timestamp of action
76
+ - Automatically set
77
+ - Used for chronological sorting and retention
78
+
79
+ ## Tracked Actions
80
+
81
+ ### Account Management Actions
82
+
83
+ #### `account_connected`
84
+
85
+ **When:** User successfully connects social media account via OAuth
86
+
87
+ **Details Structure:**
88
+ ```json
89
+ {
90
+ "platform": "instagram_business",
91
+ "accountName": "@brandname",
92
+ "accountId": "17841401234567890",
93
+ "clientId": "550e8400-e29b-41d4-a716-446655440000",
94
+ "permissions": ["pages_show_list", "instagram_basic", "instagram_content_publish"],
95
+ "tokenExpiresAt": "2024-03-15T10:30:00Z"
96
+ }
97
+ ```
98
+
99
+ **Example:**
100
+ ```typescript
101
+ await query(`
102
+ INSERT INTO "audit_logs" (
103
+ "userId", "accountId", action, details, "ipAddress", "userAgent"
104
+ ) VALUES ($1, $2, $3, $4, $5, $6)
105
+ `, [
106
+ userId,
107
+ accountId,
108
+ 'account_connected',
109
+ JSON.stringify({
110
+ platform: 'instagram_business',
111
+ accountName: '@brandname',
112
+ clientId: clientId
113
+ }),
114
+ request.headers.get('x-forwarded-for'),
115
+ request.headers.get('user-agent')
116
+ ])
117
+ ```
118
+
119
+ #### `account_disconnected`
120
+
121
+ **When:** User removes social media account
122
+
123
+ **Details Structure:**
124
+ ```json
125
+ {
126
+ "platform": "facebook_page",
127
+ "accountName": "My Business Page",
128
+ "reason": "user_requested",
129
+ "hadActiveTokens": true
130
+ }
131
+ ```
132
+
133
+ ### Publishing Actions
134
+
135
+ #### `post_published`
136
+
137
+ **When:** Content successfully published to social platform
138
+
139
+ **Details Structure:**
140
+ ```json
141
+ {
142
+ "platform": "instagram_business",
143
+ "accountName": "@brandname",
144
+ "postId": "17899618652010220",
145
+ "postUrl": "https://www.instagram.com/p/ABC123",
146
+ "imageUrl": "https://cdn.example.com/image.jpg",
147
+ "caption": "My awesome post! #instagram",
148
+ "captionLength": 35,
149
+ "publishedAt": "2024-01-15T14:30:00Z",
150
+ "success": true
151
+ }
152
+ ```
153
+
154
+ **Example:**
155
+ ```typescript
156
+ await query(`
157
+ INSERT INTO "audit_logs" (
158
+ "userId", "accountId", action, details, "ipAddress", "userAgent"
159
+ ) VALUES ($1, $2::uuid, $3, $4, $5, $6)
160
+ `, [
161
+ userId,
162
+ accountId,
163
+ 'post_published',
164
+ JSON.stringify({
165
+ platform: 'instagram_business',
166
+ accountName: account.platformAccountName,
167
+ postId: result.postId,
168
+ postUrl: result.postUrl,
169
+ imageUrl: imageUrl,
170
+ caption: caption || '',
171
+ publishedAt: new Date().toISOString(),
172
+ success: true
173
+ }),
174
+ request.headers.get('x-forwarded-for'),
175
+ request.headers.get('user-agent')
176
+ ])
177
+ ```
178
+
179
+ #### `post_failed`
180
+
181
+ **When:** Publishing attempt fails
182
+
183
+ **Details Structure:**
184
+ ```json
185
+ {
186
+ "platform": "facebook_page",
187
+ "accountName": "My Business Page",
188
+ "error": "Invalid parameter",
189
+ "errorCode": 100,
190
+ "errorDetails": {
191
+ "message": "Image URL must be publicly accessible",
192
+ "type": "OAuthException"
193
+ },
194
+ "imageUrl": "https://private.example.com/image.jpg",
195
+ "caption": "Failed post caption",
196
+ "attemptedAt": "2024-01-15T14:30:00Z"
197
+ }
198
+ ```
199
+
200
+ ### Token Management Actions
201
+
202
+ #### `token_refreshed`
203
+
204
+ **When:** OAuth token automatically refreshed
205
+
206
+ **Details Structure:**
207
+ ```json
208
+ {
209
+ "platform": "instagram_business",
210
+ "accountName": "@brandname",
211
+ "oldTokenExpiresAt": "2024-01-20T10:30:00Z",
212
+ "newTokenExpiresAt": "2024-03-21T10:30:00Z",
213
+ "minutesBeforeExpiry": 8,
214
+ "refreshMethod": "automatic"
215
+ }
216
+ ```
217
+
218
+ **Example:**
219
+ ```typescript
220
+ await query(`
221
+ INSERT INTO "audit_logs" (
222
+ "userId", "accountId", action, details
223
+ ) VALUES ($1, $2, $3, $4)
224
+ `, [
225
+ 'system', // Automated action
226
+ accountId,
227
+ 'token_refreshed',
228
+ JSON.stringify({
229
+ platform: account.platform,
230
+ accountName: account.platformAccountName,
231
+ oldTokenExpiresAt: oldExpiresAt,
232
+ newTokenExpiresAt: newExpiresAt,
233
+ minutesBeforeExpiry: minutesLeft,
234
+ refreshMethod: 'automatic'
235
+ })
236
+ ])
237
+ ```
238
+
239
+ #### `token_refresh_failed`
240
+
241
+ **When:** Token refresh attempt fails
242
+
243
+ **Details Structure:**
244
+ ```json
245
+ {
246
+ "platform": "facebook_page",
247
+ "accountName": "My Business Page",
248
+ "error": "Invalid OAuth 2.0 Access Token",
249
+ "tokenExpiresAt": "2024-01-15T10:30:00Z",
250
+ "minutesBeforeExpiry": 5,
251
+ "requiresReconnection": true
252
+ }
253
+ ```
254
+
255
+ ## Querying Audit Logs
256
+
257
+ ### Get User's Activity
258
+
259
+ ```typescript
260
+ async function getUserActivity(
261
+ userId: string,
262
+ limit: number = 50
263
+ ): Promise<AuditLog[]> {
264
+ const result = await query(`
265
+ SELECT * FROM "audit_logs"
266
+ WHERE "userId" = $1
267
+ ORDER BY "createdAt" DESC
268
+ LIMIT $2
269
+ `, [userId, limit])
270
+
271
+ return result.rows
272
+ }
273
+ ```
274
+
275
+ ### Get Account Activity
276
+
277
+ ```typescript
278
+ async function getAccountActivity(
279
+ accountId: string
280
+ ): Promise<AuditLog[]> {
281
+ const result = await query(`
282
+ SELECT * FROM "audit_logs"
283
+ WHERE "accountId" = $1
284
+ ORDER BY "createdAt" DESC
285
+ `, [accountId])
286
+
287
+ return result.rows
288
+ }
289
+ ```
290
+
291
+ ### Get Failed Publish Attempts
292
+
293
+ ```typescript
294
+ async function getFailedPublishAttempts(
295
+ userId: string,
296
+ days: number = 30
297
+ ): Promise<AuditLog[]> {
298
+ const result = await query(`
299
+ SELECT * FROM "audit_logs"
300
+ WHERE "userId" = $1
301
+ AND action = 'post_failed'
302
+ AND "createdAt" > NOW() - INTERVAL '${days} days'
303
+ ORDER BY "createdAt" DESC
304
+ `, [userId])
305
+
306
+ return result.rows
307
+ }
308
+ ```
309
+
310
+ ### Get Token Refresh History
311
+
312
+ ```typescript
313
+ async function getTokenRefreshHistory(
314
+ accountId: string
315
+ ): Promise<AuditLog[]> {
316
+ const result = await query(`
317
+ SELECT * FROM "audit_logs"
318
+ WHERE "accountId" = $1
319
+ AND action IN ('token_refreshed', 'token_refresh_failed')
320
+ ORDER BY "createdAt" DESC
321
+ `, [accountId])
322
+
323
+ return result.rows
324
+ }
325
+ ```
326
+
327
+ ### Activity By Action Type
328
+
329
+ ```typescript
330
+ async function getActivityByAction(
331
+ userId: string,
332
+ action: string,
333
+ startDate: Date,
334
+ endDate: Date
335
+ ): Promise<AuditLog[]> {
336
+ const result = await query(`
337
+ SELECT * FROM "audit_logs"
338
+ WHERE "userId" = $1
339
+ AND action = $2
340
+ AND "createdAt" BETWEEN $3 AND $4
341
+ ORDER BY "createdAt" DESC
342
+ `, [userId, action, startDate, endDate])
343
+
344
+ return result.rows
345
+ }
346
+ ```
347
+
348
+ ## Reporting and Analytics
349
+
350
+ ### Publishing Success Rate
351
+
352
+ ```sql
353
+ SELECT
354
+ DATE("createdAt") as date,
355
+ COUNT(*) FILTER (WHERE action = 'post_published') as successful,
356
+ COUNT(*) FILTER (WHERE action = 'post_failed') as failed,
357
+ ROUND(
358
+ COUNT(*) FILTER (WHERE action = 'post_published')::decimal /
359
+ NULLIF(COUNT(*), 0) * 100,
360
+ 2
361
+ ) as success_rate
362
+ FROM "audit_logs"
363
+ WHERE action IN ('post_published', 'post_failed')
364
+ AND "createdAt" > NOW() - INTERVAL '30 days'
365
+ GROUP BY DATE("createdAt")
366
+ ORDER BY date DESC;
367
+ ```
368
+
369
+ ### Most Active Users
370
+
371
+ ```sql
372
+ SELECT
373
+ "userId",
374
+ COUNT(*) FILTER (WHERE action = 'post_published') as posts_published,
375
+ COUNT(*) FILTER (WHERE action = 'account_connected') as accounts_connected,
376
+ COUNT(*) as total_actions
377
+ FROM "audit_logs"
378
+ WHERE "createdAt" > NOW() - INTERVAL '30 days'
379
+ GROUP BY "userId"
380
+ ORDER BY total_actions DESC
381
+ LIMIT 10;
382
+ ```
383
+
384
+ ### Platform Distribution
385
+
386
+ ```sql
387
+ SELECT
388
+ details->>'platform' as platform,
389
+ COUNT(*) as publish_count
390
+ FROM "audit_logs"
391
+ WHERE action = 'post_published'
392
+ AND "createdAt" > NOW() - INTERVAL '30 days'
393
+ GROUP BY details->>'platform'
394
+ ORDER BY publish_count DESC;
395
+ ```
396
+
397
+ ### Error Analysis
398
+
399
+ ```sql
400
+ SELECT
401
+ details->>'error' as error_message,
402
+ details->>'platform' as platform,
403
+ COUNT(*) as occurrence_count,
404
+ MAX("createdAt") as last_occurred
405
+ FROM "audit_logs"
406
+ WHERE action = 'post_failed'
407
+ AND "createdAt" > NOW() - INTERVAL '7 days'
408
+ GROUP BY details->>'error', details->>'platform'
409
+ ORDER BY occurrence_count DESC;
410
+ ```
411
+
412
+ ## Compliance and Retention
413
+
414
+ ### GDPR Compliance
415
+
416
+ **Right to Access:**
417
+ ```typescript
418
+ async function exportUserAuditLogs(userId: string): Promise<AuditLog[]> {
419
+ const result = await query(`
420
+ SELECT * FROM "audit_logs"
421
+ WHERE "userId" = $1
422
+ ORDER BY "createdAt" DESC
423
+ `, [userId])
424
+
425
+ return result.rows
426
+ }
427
+ ```
428
+
429
+ **Right to Erasure:**
430
+ ```typescript
431
+ async function anonymizeUserAuditLogs(userId: string): Promise<void> {
432
+ // Anonymize personal data while keeping audit trail
433
+ await query(`
434
+ UPDATE "audit_logs"
435
+ SET "userId" = 'anonymized',
436
+ "ipAddress" = NULL,
437
+ "userAgent" = NULL,
438
+ details = jsonb_set(
439
+ details,
440
+ '{userId}',
441
+ '"anonymized"'
442
+ )
443
+ WHERE "userId" = $1
444
+ `, [userId])
445
+ }
446
+ ```
447
+
448
+ ### Retention Policy
449
+
450
+ **Recommended Policy:**
451
+ - **Active Logs:** Keep 1 year online
452
+ - **Archive:** Move to cold storage after 1 year
453
+ - **Deletion:** Delete after 7 years (or per regulations)
454
+
455
+ **Archive Implementation:**
456
+ ```sql
457
+ -- Create archive table
458
+ CREATE TABLE "audit_logs_archive" (
459
+ LIKE "audit_logs" INCLUDING ALL
460
+ );
461
+
462
+ -- Move old logs to archive
463
+ INSERT INTO "audit_logs_archive"
464
+ SELECT * FROM "audit_logs"
465
+ WHERE "createdAt" < NOW() - INTERVAL '1 year';
466
+
467
+ -- Delete from main table
468
+ DELETE FROM "audit_logs"
469
+ WHERE "createdAt" < NOW() - INTERVAL '1 year';
470
+ ```
471
+
472
+ **Automated Retention:**
473
+ ```typescript
474
+ // Run monthly via cron
475
+ async function enforceRetentionPolicy(): Promise<void> {
476
+ // Archive logs older than 1 year
477
+ await query(`
478
+ INSERT INTO "audit_logs_archive"
479
+ SELECT * FROM "audit_logs"
480
+ WHERE "createdAt" < NOW() - INTERVAL '1 year'
481
+ `)
482
+
483
+ // Delete archived logs
484
+ await query(`
485
+ DELETE FROM "audit_logs"
486
+ WHERE "createdAt" < NOW() - INTERVAL '1 year'
487
+ `)
488
+
489
+ // Delete archive logs older than 7 years
490
+ await query(`
491
+ DELETE FROM "audit_logs_archive"
492
+ WHERE "createdAt" < NOW() - INTERVAL '7 years'
493
+ `)
494
+ }
495
+ ```
496
+
497
+ ## Monitoring and Alerting
498
+
499
+ ### Key Metrics to Monitor
500
+
501
+ **1. Failed Publish Rate:**
502
+ ```sql
503
+ SELECT
504
+ ROUND(
505
+ COUNT(*) FILTER (WHERE action = 'post_failed')::decimal /
506
+ NULLIF(COUNT(*), 0) * 100,
507
+ 2
508
+ ) as failed_rate
509
+ FROM "audit_logs"
510
+ WHERE action IN ('post_published', 'post_failed')
511
+ AND "createdAt" > NOW() - INTERVAL '1 hour';
512
+ ```
513
+
514
+ **Alert If:** Failed rate > 10%
515
+
516
+ **2. Token Refresh Failures:**
517
+ ```sql
518
+ SELECT COUNT(*) as failures
519
+ FROM "audit_logs"
520
+ WHERE action = 'token_refresh_failed'
521
+ AND "createdAt" > NOW() - INTERVAL '1 day';
522
+ ```
523
+
524
+ **Alert If:** > 5 failures per day
525
+
526
+ **3. Unusual Activity:**
527
+ ```sql
528
+ -- Detect accounts with unusually high publish volume
529
+ SELECT
530
+ "accountId",
531
+ COUNT(*) as publish_count
532
+ FROM "audit_logs"
533
+ WHERE action = 'post_published'
534
+ AND "createdAt" > NOW() - INTERVAL '1 hour'
535
+ GROUP BY "accountId"
536
+ HAVING COUNT(*) > 10; -- More than 10 posts per hour
537
+ ```
538
+
539
+ **Alert If:** Unusual spikes detected (possible abuse)
540
+
541
+ ### Alerting Implementation
542
+
543
+ ```typescript
544
+ async function checkAuditAlertsAndNotify(): Promise<void> {
545
+ // Check failed publish rate
546
+ const failedRate = await getFailedPublishRate()
547
+ if (failedRate > 10) {
548
+ await sendAlert('high-failed-rate', { rate: failedRate })
549
+ }
550
+
551
+ // Check token refresh failures
552
+ const tokenFailures = await getTokenRefreshFailures()
553
+ if (tokenFailures > 5) {
554
+ await sendAlert('token-refresh-failures', { count: tokenFailures })
555
+ }
556
+
557
+ // Check for unusual activity
558
+ const unusualAccounts = await getUnusualActivity()
559
+ if (unusualAccounts.length > 0) {
560
+ await sendAlert('unusual-activity', { accounts: unusualAccounts })
561
+ }
562
+ }
563
+ ```
564
+
565
+ ## Best Practices
566
+
567
+ ### Do's ✅
568
+
569
+ **1. Always Log Critical Actions:**
570
+ ```typescript
571
+ // ✅ Good: Log every publish attempt
572
+ await logAudit('post_published', { ... })
573
+ ```
574
+
575
+ **2. Include Full Context:**
576
+ ```typescript
577
+ // ✅ Good: Rich details
578
+ details: {
579
+ platform: 'instagram',
580
+ accountName: '@brand',
581
+ postId: '123',
582
+ imageUrl: 'https://...',
583
+ caption: '...',
584
+ success: true
585
+ }
586
+
587
+ // ❌ Bad: Minimal details
588
+ details: { success: true }
589
+ ```
590
+
591
+ **3. Capture IP and User-Agent:**
592
+ ```typescript
593
+ // ✅ Good
594
+ ipAddress: request.headers.get('x-forwarded-for'),
595
+ userAgent: request.headers.get('user-agent')
596
+ ```
597
+
598
+ **4. Use Consistent Action Names:**
599
+ ```typescript
600
+ // ✅ Good: Standardized
601
+ 'account_connected', 'post_published', 'token_refreshed'
602
+
603
+ // ❌ Bad: Inconsistent
604
+ 'ACCOUNT_CONNECTED', 'post-published', 'Token Refreshed'
605
+ ```
606
+
607
+ ### Don'ts ❌
608
+
609
+ **1. Don't Log Sensitive Data:**
610
+ ```typescript
611
+ // ❌ Bad: Logs token
612
+ details: { token: decryptedToken }
613
+
614
+ // ✅ Good: Never log tokens
615
+ details: { tokenRefreshed: true }
616
+ ```
617
+
618
+ **2. Don't Allow Log Deletion:**
619
+ ```sql
620
+ -- ❌ Bad: DELETE operations allowed
621
+ DELETE FROM "audit_logs" WHERE id = $1;
622
+
623
+ -- ✅ Good: No DELETE, only INSERT
624
+ -- Enforce via database permissions
625
+ ```
626
+
627
+ **3. Don't Skip Error Logging:**
628
+ ```typescript
629
+ // ❌ Bad
630
+ if (error) {
631
+ console.error(error)
632
+ return // No audit log
633
+ }
634
+
635
+ // ✅ Good
636
+ if (error) {
637
+ await logAudit('post_failed', { error: error.message })
638
+ return
639
+ }
640
+ ```
641
+
642
+ ## Next Steps
643
+
644
+ - **[Publishing](./02-publishing.md)** - See how audit logging integrates with publishing
645
+ - **[Token Management](./03-token-management.md)** - Token refresh audit logs
646
+ - **[Per-Client Architecture](../03-advanced-usage/03-per-client-architecture.md)** - Understand data model