@happyvertical/smrt-social 0.30.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 (47) hide show
  1. package/AGENTS.md +18 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +94 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/collections/index.d.ts +5 -0
  8. package/dist/collections/index.d.ts.map +1 -0
  9. package/dist/collections/oauth-state-collection.d.ts +9 -0
  10. package/dist/collections/oauth-state-collection.d.ts.map +1 -0
  11. package/dist/collections/social-account-collection.d.ts +9 -0
  12. package/dist/collections/social-account-collection.d.ts.map +1 -0
  13. package/dist/collections/social-post-analytics-snapshot-collection.d.ts +18 -0
  14. package/dist/collections/social-post-analytics-snapshot-collection.d.ts.map +1 -0
  15. package/dist/collections/social-post-collection.d.ts +32 -0
  16. package/dist/collections/social-post-collection.d.ts.map +1 -0
  17. package/dist/index.d.ts +6 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +824 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/manifest.json +3363 -0
  22. package/dist/oauth-state.d.ts +127 -0
  23. package/dist/oauth-state.d.ts.map +1 -0
  24. package/dist/server.d.ts +18 -0
  25. package/dist/server.d.ts.map +1 -0
  26. package/dist/server.js +106 -0
  27. package/dist/server.js.map +1 -0
  28. package/dist/smrt-knowledge.json +1398 -0
  29. package/dist/social-account.d.ts +270 -0
  30. package/dist/social-account.d.ts.map +1 -0
  31. package/dist/social-post-analytics-snapshot.d.ts +40 -0
  32. package/dist/social-post-analytics-snapshot.d.ts.map +1 -0
  33. package/dist/social-post.d.ts +258 -0
  34. package/dist/social-post.d.ts.map +1 -0
  35. package/dist/svelte/components/SocialAccountSettings.svelte +243 -0
  36. package/dist/svelte/components/SocialAccountSettings.svelte.d.ts +15 -0
  37. package/dist/svelte/components/SocialAccountSettings.svelte.d.ts.map +1 -0
  38. package/dist/svelte/i18n.d.ts +9 -0
  39. package/dist/svelte/i18n.d.ts.map +1 -0
  40. package/dist/svelte/i18n.js +15 -0
  41. package/dist/svelte/index.d.ts +6 -0
  42. package/dist/svelte/index.d.ts.map +1 -0
  43. package/dist/svelte/index.js +2 -0
  44. package/dist/svelte/types.d.ts +16 -0
  45. package/dist/svelte/types.d.ts.map +1 -0
  46. package/dist/svelte/types.js +1 -0
  47. package/package.json +88 -0
package/dist/index.js ADDED
@@ -0,0 +1,824 @@
1
+ import { ObjectRegistry, smrt, SmrtObject, SmrtCollection, foreignKey, crossPackageRef, field } from "@happyvertical/smrt-core";
2
+ import { tenantId, TenantScoped, getCurrentTenant, withTenant } from "@happyvertical/smrt-tenancy";
3
+ import { randomUUID } from "node:crypto";
4
+ import { SecretService } from "@happyvertical/smrt-secrets";
5
+ import { getSocialPlatformSetup } from "./server.js";
6
+ import { VideoContent } from "@happyvertical/smrt-video";
7
+ ObjectRegistry.registerPackageManifest(
8
+ new URL("./manifest.json", import.meta.url)
9
+ );
10
+ var __defProp$3 = Object.defineProperty;
11
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
12
+ var __decorateClass$3 = (decorators, target, key, kind) => {
13
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
14
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
15
+ if (decorator = decorators[i])
16
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
17
+ if (kind && result) __defProp$3(target, key, result);
18
+ return result;
19
+ };
20
+ let OAuthState = class extends SmrtObject {
21
+ tenantId = null;
22
+ /**
23
+ * Platform being connected
24
+ */
25
+ platform = "youtube";
26
+ /**
27
+ * CSRF state token
28
+ * This is sent to the OAuth provider and verified on callback
29
+ */
30
+ state = "";
31
+ /**
32
+ * PKCE code verifier
33
+ * Required for platforms using PKCE (YouTube, etc.)
34
+ */
35
+ codeVerifier = null;
36
+ /**
37
+ * Redirect URI used in the OAuth request
38
+ * Must match on callback for verification
39
+ */
40
+ redirectUri = "";
41
+ /**
42
+ * Requested OAuth scopes
43
+ */
44
+ scopes = [];
45
+ /**
46
+ * When this state expires
47
+ * States should be short-lived (10 minutes typical)
48
+ */
49
+ expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
50
+ constructor(options = {}) {
51
+ super(options);
52
+ if (options.platform !== void 0) this.platform = options.platform;
53
+ if (options.state !== void 0) this.state = options.state;
54
+ if (options.codeVerifier !== void 0)
55
+ this.codeVerifier = options.codeVerifier;
56
+ if (options.redirectUri !== void 0)
57
+ this.redirectUri = options.redirectUri;
58
+ if (options.scopes !== void 0) this.scopes = options.scopes;
59
+ if (options.expiresAt !== void 0) this.expiresAt = options.expiresAt;
60
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
61
+ }
62
+ /**
63
+ * Check if the state has expired
64
+ */
65
+ get isExpired() {
66
+ return Date.now() >= this.expiresAt.getTime();
67
+ }
68
+ /**
69
+ * Check if the state is still valid
70
+ */
71
+ get isValid() {
72
+ return !this.isExpired && this.state.length > 0;
73
+ }
74
+ /**
75
+ * Verify a callback state matches this record
76
+ */
77
+ verifyState(callbackState) {
78
+ return this.isValid && this.state === callbackState;
79
+ }
80
+ /**
81
+ * Generate a new random state token
82
+ */
83
+ static generateState() {
84
+ return crypto.randomUUID();
85
+ }
86
+ /**
87
+ * Generate a PKCE code verifier
88
+ * Returns a 43-128 character random string
89
+ */
90
+ static generateCodeVerifier() {
91
+ const array = new Uint8Array(32);
92
+ crypto.getRandomValues(array);
93
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
94
+ ""
95
+ );
96
+ }
97
+ /**
98
+ * Generate PKCE code challenge from verifier (S256 method)
99
+ * Note: This requires async crypto operations
100
+ */
101
+ static async generateCodeChallenge(verifier) {
102
+ const encoder = new TextEncoder();
103
+ const data = encoder.encode(verifier);
104
+ const digest = await crypto.subtle.digest("SHA-256", data);
105
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(digest)));
106
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
107
+ }
108
+ };
109
+ __decorateClass$3([
110
+ tenantId({ nullable: true })
111
+ ], OAuthState.prototype, "tenantId", 2);
112
+ OAuthState = __decorateClass$3([
113
+ TenantScoped({ mode: "optional" }),
114
+ smrt({
115
+ tableStrategy: "sti",
116
+ api: {
117
+ include: ["list", "get", "create", "delete"]
118
+ },
119
+ mcp: false,
120
+ cli: true
121
+ })
122
+ ], OAuthState);
123
+ class OAuthStateCollection extends SmrtCollection {
124
+ static _itemClass = OAuthState;
125
+ async findByState(state) {
126
+ return this.get({ state });
127
+ }
128
+ async findExpired(now = /* @__PURE__ */ new Date()) {
129
+ return this.list({
130
+ where: { "expiresAt <=": now },
131
+ orderBy: "expiresAt ASC"
132
+ });
133
+ }
134
+ async deleteExpired(now = /* @__PURE__ */ new Date()) {
135
+ const expired = await this.findExpired(now);
136
+ await Promise.all(expired.map((state) => state.delete()));
137
+ return expired.length;
138
+ }
139
+ }
140
+ var __defProp$2 = Object.defineProperty;
141
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
142
+ var __decorateClass$2 = (decorators, target, key, kind) => {
143
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
144
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
145
+ if (decorator = decorators[i])
146
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
147
+ if (kind && result) __defProp$2(target, key, result);
148
+ return result;
149
+ };
150
+ let SocialAccount = class extends SmrtObject {
151
+ tenantId = null;
152
+ /**
153
+ * Human-readable name for the account
154
+ */
155
+ name = "";
156
+ /**
157
+ * Social platform type
158
+ */
159
+ platform = "youtube";
160
+ /**
161
+ * Platform-specific user ID
162
+ */
163
+ platformUserId = null;
164
+ /**
165
+ * Platform username/handle
166
+ */
167
+ platformUsername = null;
168
+ /**
169
+ * Profile URL on the platform
170
+ */
171
+ platformUrl = null;
172
+ /**
173
+ * Deprecated raw OAuth access token.
174
+ * Prefer credentialSecretId/accessTokenSecretName.
175
+ */
176
+ accessToken = null;
177
+ /**
178
+ * Deprecated raw OAuth refresh token.
179
+ * Prefer credentialSecretId/refreshTokenSecretName.
180
+ */
181
+ refreshToken = null;
182
+ /**
183
+ * Secret name containing the complete platform credential payload.
184
+ */
185
+ credentialSecretId = null;
186
+ /**
187
+ * Secret name containing only the access token.
188
+ */
189
+ accessTokenSecretName = null;
190
+ /**
191
+ * Secret name containing only the refresh token.
192
+ */
193
+ refreshTokenSecretName = null;
194
+ /**
195
+ * Token expiration time
196
+ */
197
+ tokenExpiresAt = null;
198
+ /**
199
+ * Whether the account is active
200
+ */
201
+ isActive = true;
202
+ /**
203
+ * Default hashtags to add to posts
204
+ */
205
+ defaultHashtags = [];
206
+ /**
207
+ * Granted OAuth scopes or platform permissions.
208
+ */
209
+ scopes = [];
210
+ /**
211
+ * Required permissions that still need app review or user grant.
212
+ */
213
+ missingPermissions = [];
214
+ /**
215
+ * How to handle links in posts
216
+ * - description: Include link in post body/description
217
+ * - reply: Post link as a reply (better for X algorithm)
218
+ * - none: Don't include link
219
+ */
220
+ linkBehavior = "description";
221
+ /**
222
+ * Safety mode for publish operations.
223
+ */
224
+ publishMode = "dry_run";
225
+ /**
226
+ * Separate latch required before public publishing is allowed.
227
+ */
228
+ publicPublishingAllowed = false;
229
+ /**
230
+ * Account connection status
231
+ */
232
+ status = "connected";
233
+ /**
234
+ * Error message if status is 'error'
235
+ */
236
+ errorMessage = null;
237
+ constructor(options = {}) {
238
+ super(options);
239
+ if (options.name !== void 0) this.name = options.name;
240
+ if (options.platform !== void 0) this.platform = options.platform;
241
+ if (options.platformUserId !== void 0)
242
+ this.platformUserId = options.platformUserId;
243
+ if (options.platformUsername !== void 0)
244
+ this.platformUsername = options.platformUsername;
245
+ if (options.platformUrl !== void 0)
246
+ this.platformUrl = options.platformUrl;
247
+ if (options.accessToken !== void 0)
248
+ this.accessToken = options.accessToken;
249
+ if (options.refreshToken !== void 0)
250
+ this.refreshToken = options.refreshToken;
251
+ if (options.credentialSecretId !== void 0)
252
+ this.credentialSecretId = options.credentialSecretId;
253
+ if (options.accessTokenSecretName !== void 0)
254
+ this.accessTokenSecretName = options.accessTokenSecretName;
255
+ if (options.refreshTokenSecretName !== void 0)
256
+ this.refreshTokenSecretName = options.refreshTokenSecretName;
257
+ if (options.tokenExpiresAt !== void 0)
258
+ this.tokenExpiresAt = options.tokenExpiresAt;
259
+ if (options.isActive !== void 0) this.isActive = options.isActive;
260
+ if (options.defaultHashtags !== void 0)
261
+ this.defaultHashtags = options.defaultHashtags;
262
+ if (options.scopes !== void 0) this.scopes = options.scopes;
263
+ if (options.missingPermissions !== void 0)
264
+ this.missingPermissions = options.missingPermissions;
265
+ if (options.linkBehavior !== void 0)
266
+ this.linkBehavior = options.linkBehavior;
267
+ if (options.publishMode !== void 0)
268
+ this.publishMode = options.publishMode;
269
+ if (options.publicPublishingAllowed !== void 0)
270
+ this.publicPublishingAllowed = options.publicPublishingAllowed;
271
+ if (options.status !== void 0) this.status = options.status;
272
+ if (options.errorMessage !== void 0)
273
+ this.errorMessage = options.errorMessage;
274
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
275
+ if (options.platform !== void 0) {
276
+ const setup = getSocialPlatformSetup(this.platform);
277
+ if (options.linkBehavior === void 0) {
278
+ this.linkBehavior = setup.defaultLinkBehavior;
279
+ }
280
+ if (options.publishMode === void 0) {
281
+ this.publishMode = setup.defaultPublishMode;
282
+ }
283
+ }
284
+ }
285
+ /**
286
+ * Social accounts need a slug identity that is scoped by tenant and platform.
287
+ * A newsroom may connect `@localnews` on X, YouTube, Threads, and Facebook;
288
+ * the generic name-derived slug would make those accounts overwrite each
289
+ * other through SMRT's slug/context upsert identity.
290
+ */
291
+ async getSlug() {
292
+ if (!this.slug) {
293
+ const identity = this.platformUserId || this.platformUsername || this.name || this.id;
294
+ if (!identity) {
295
+ return this.slug;
296
+ }
297
+ const source = [
298
+ this.tenantId ? `tenant-${this.tenantId}` : "no-tenant",
299
+ this.platform,
300
+ identity
301
+ ].filter(Boolean).join("-");
302
+ this.slug = source.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
303
+ }
304
+ return this.slug;
305
+ }
306
+ /**
307
+ * Check if the token is expired or will expire soon
308
+ */
309
+ get isTokenExpired() {
310
+ if (!this.tokenExpiresAt) return false;
311
+ const expiryBuffer = 5 * 60 * 1e3;
312
+ return Date.now() >= this.tokenExpiresAt.getTime() - expiryBuffer;
313
+ }
314
+ /**
315
+ * Check if the account needs attention (expired or error)
316
+ */
317
+ get needsAttention() {
318
+ return this.status !== "connected" || this.isTokenExpired || this.missingPermissions.length > 0 || this.publishMode === "public" && !this.publicPublishingAllowed;
319
+ }
320
+ /**
321
+ * Check if the account is ready for publishing
322
+ */
323
+ get isReady() {
324
+ return this.isActive && this.status === "connected" && this.hasCredentials && this.missingPermissions.length === 0 && !this.isTokenExpired && (this.publishMode !== "public" || this.publicPublishingAllowed);
325
+ }
326
+ /**
327
+ * Effective publish mode after applying the public-publishing latch.
328
+ */
329
+ get effectivePublishMode() {
330
+ if (this.publishMode === "public" && !this.publicPublishingAllowed) {
331
+ return "dry_run";
332
+ }
333
+ return this.publishMode;
334
+ }
335
+ /**
336
+ * Check whether any usable credential reference exists.
337
+ */
338
+ get hasCredentials() {
339
+ return Boolean(
340
+ this.credentialSecretId || this.accessTokenSecretName || this.accessToken
341
+ );
342
+ }
343
+ /**
344
+ * Store all platform credentials in smrt-secrets as a single JSON payload.
345
+ */
346
+ async setCredentials(credentials, options = {}) {
347
+ if (!this.id) {
348
+ this.id = randomUUID();
349
+ }
350
+ const secretName = this.credentialSecretId ?? `social-account-${this.id}`;
351
+ const tenantId2 = this.requireCredentialTenantId(
352
+ "store social account credentials"
353
+ );
354
+ const secretService = await SecretService.create({ db: this.db });
355
+ await secretService.storeForTenant(
356
+ tenantId2,
357
+ secretName,
358
+ JSON.stringify(credentials),
359
+ {
360
+ description: options.description ?? `Credentials for ${this.name}`,
361
+ category: options.category ?? "social"
362
+ }
363
+ );
364
+ this.credentialSecretId = secretName;
365
+ await this.withCredentialTenantContext(async () => {
366
+ await this.save();
367
+ });
368
+ }
369
+ /**
370
+ * Retrieve platform credentials from smrt-secrets, falling back to deprecated fields.
371
+ */
372
+ async getCredentials() {
373
+ if (!this.credentialSecretId) {
374
+ if (this.accessTokenSecretName || this.refreshTokenSecretName) {
375
+ const tenantId3 = this.requireCredentialTenantId(
376
+ "retrieve social account credentials"
377
+ );
378
+ const secretService2 = await SecretService.create({ db: this.db });
379
+ const credentials = {};
380
+ if (this.accessTokenSecretName) {
381
+ const secret2 = await secretService2.retrieveForTenant(
382
+ tenantId3,
383
+ this.accessTokenSecretName
384
+ );
385
+ credentials.accessToken = secret2.value;
386
+ }
387
+ if (this.refreshTokenSecretName) {
388
+ const secret2 = await secretService2.retrieveForTenant(
389
+ tenantId3,
390
+ this.refreshTokenSecretName
391
+ );
392
+ credentials.refreshToken = secret2.value;
393
+ }
394
+ return credentials;
395
+ }
396
+ if (!this.accessToken && !this.refreshToken) return null;
397
+ return {
398
+ accessToken: this.accessToken,
399
+ refreshToken: this.refreshToken
400
+ };
401
+ }
402
+ const credentialSecretId = this.credentialSecretId;
403
+ const tenantId2 = this.requireCredentialTenantId(
404
+ "retrieve social account credentials"
405
+ );
406
+ const secretService = await SecretService.create({ db: this.db });
407
+ const secret = await secretService.retrieveForTenant(
408
+ tenantId2,
409
+ credentialSecretId
410
+ );
411
+ return JSON.parse(secret.value);
412
+ }
413
+ async withCredentialTenantContext(fn) {
414
+ const tenantId2 = this.getCredentialTenantId();
415
+ const currentTenantId = getCurrentTenant()?.tenantId;
416
+ if (!tenantId2 || currentTenantId === tenantId2) {
417
+ return fn();
418
+ }
419
+ return withTenant({ tenantId: tenantId2 }, fn);
420
+ }
421
+ getCredentialTenantId() {
422
+ if (this.tenantId) {
423
+ return this.tenantId;
424
+ }
425
+ return getCurrentTenant()?.tenantId ?? null;
426
+ }
427
+ requireCredentialTenantId(action) {
428
+ const tenantId2 = this.getCredentialTenantId();
429
+ if (!tenantId2) {
430
+ throw new Error(`Tenant context required to ${action}.`);
431
+ }
432
+ return tenantId2;
433
+ }
434
+ };
435
+ __decorateClass$2([
436
+ tenantId({ nullable: true })
437
+ ], SocialAccount.prototype, "tenantId", 2);
438
+ SocialAccount = __decorateClass$2([
439
+ TenantScoped({ mode: "optional" }),
440
+ smrt({
441
+ tableStrategy: "sti",
442
+ api: {
443
+ include: ["list", "get", "create", "update", "delete"]
444
+ },
445
+ mcp: {
446
+ include: ["list", "get"]
447
+ },
448
+ cli: true
449
+ })
450
+ ], SocialAccount);
451
+ class SocialAccountCollection extends SmrtCollection {
452
+ static _itemClass = SocialAccount;
453
+ async findActive(platform) {
454
+ const accounts = await this.list({
455
+ where: {
456
+ isActive: true,
457
+ ...platform ? { platform } : {}
458
+ },
459
+ orderBy: "name ASC"
460
+ });
461
+ return accounts;
462
+ }
463
+ async findReady(platform) {
464
+ const accounts = await this.findActive(platform);
465
+ return accounts.filter((account) => account.isReady);
466
+ }
467
+ async findNeedsAttention() {
468
+ const accounts = await this.list({
469
+ where: { isActive: true },
470
+ orderBy: "name ASC"
471
+ });
472
+ return accounts.filter((account) => account.needsAttention);
473
+ }
474
+ }
475
+ var __defProp$1 = Object.defineProperty;
476
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
477
+ var __decorateClass$1 = (decorators, target, key, kind) => {
478
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
479
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
480
+ if (decorator = decorators[i])
481
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
482
+ if (kind && result) __defProp$1(target, key, result);
483
+ return result;
484
+ };
485
+ let SocialPost = class extends SmrtObject {
486
+ tenantId = null;
487
+ socialAccountId = null;
488
+ videoContentId = null;
489
+ contentId = null;
490
+ /**
491
+ * High-level post type
492
+ */
493
+ postType = "text";
494
+ /**
495
+ * Public media URL for platforms that require URL media publishing
496
+ */
497
+ mediaUrl = null;
498
+ /**
499
+ * Post title (for platforms that support it)
500
+ */
501
+ title = null;
502
+ /**
503
+ * Post description/caption
504
+ */
505
+ description = "";
506
+ /**
507
+ * Hashtags to include
508
+ */
509
+ hashtags = [];
510
+ /**
511
+ * Link URL to include in the post
512
+ */
513
+ linkUrl = null;
514
+ /**
515
+ * Platform-specific post ID (set after publishing)
516
+ */
517
+ platformPostId = null;
518
+ /**
519
+ * Public post URL (set after publishing)
520
+ */
521
+ platformUrl = null;
522
+ /**
523
+ * Scheduled publish time
524
+ * If set, post will be scheduled instead of published immediately
525
+ */
526
+ scheduledAt = null;
527
+ /**
528
+ * Actual publish time
529
+ */
530
+ publishedAt = null;
531
+ /**
532
+ * Post status
533
+ * - draft: Not yet submitted for publishing
534
+ * - pending_approval: Awaiting editorial approval
535
+ * - approved: Approved and ready to publish
536
+ * - scheduled: Queued for future publishing
537
+ * - publishing: Currently being published
538
+ * - dry_run: Payload was validated without remote write
539
+ * - staged: Non-public platform object/container was created
540
+ * - published: Successfully published
541
+ * - failed: Publishing failed
542
+ * - cancelled: Publishing was cancelled
543
+ */
544
+ status = "draft";
545
+ /**
546
+ * Error message if status is 'failed'
547
+ */
548
+ errorMessage = null;
549
+ /**
550
+ * Engagement analytics
551
+ */
552
+ analytics = {};
553
+ /**
554
+ * When analytics were last synced
555
+ */
556
+ analyticsLastSyncedAt = null;
557
+ constructor(options = {}) {
558
+ super(options);
559
+ if (options.socialAccountId !== void 0)
560
+ this.socialAccountId = options.socialAccountId;
561
+ if (options.videoContentId !== void 0)
562
+ this.videoContentId = options.videoContentId;
563
+ if (options.contentId !== void 0) this.contentId = options.contentId;
564
+ if (options.postType !== void 0) this.postType = options.postType;
565
+ if (options.mediaUrl !== void 0) this.mediaUrl = options.mediaUrl;
566
+ if (options.title !== void 0) this.title = options.title;
567
+ if (options.description !== void 0)
568
+ this.description = options.description;
569
+ if (options.hashtags !== void 0) this.hashtags = options.hashtags;
570
+ if (options.linkUrl !== void 0) this.linkUrl = options.linkUrl;
571
+ if (options.platformPostId !== void 0)
572
+ this.platformPostId = options.platformPostId;
573
+ if (options.platformUrl !== void 0)
574
+ this.platformUrl = options.platformUrl;
575
+ if (options.scheduledAt !== void 0)
576
+ this.scheduledAt = options.scheduledAt;
577
+ if (options.publishedAt !== void 0)
578
+ this.publishedAt = options.publishedAt;
579
+ if (options.status !== void 0) this.status = options.status;
580
+ if (options.errorMessage !== void 0)
581
+ this.errorMessage = options.errorMessage;
582
+ if (options.analytics !== void 0) this.analytics = options.analytics;
583
+ if (options.analyticsLastSyncedAt !== void 0)
584
+ this.analyticsLastSyncedAt = options.analyticsLastSyncedAt;
585
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
586
+ }
587
+ /**
588
+ * Check if the post is scheduled for future publishing
589
+ */
590
+ get isScheduled() {
591
+ return this.status === "scheduled" && this.scheduledAt !== null;
592
+ }
593
+ /**
594
+ * Check if the post has been published
595
+ */
596
+ get isPublished() {
597
+ return this.status === "published";
598
+ }
599
+ /**
600
+ * Check if the post can be edited (draft or failed)
601
+ */
602
+ get isEditable() {
603
+ return this.status === "draft" || this.status === "pending_approval" || this.status === "failed";
604
+ }
605
+ /**
606
+ * Check if the post is due for publishing now.
607
+ */
608
+ get isDueForPublish() {
609
+ if (this.status !== "approved" && this.status !== "scheduled") {
610
+ return false;
611
+ }
612
+ return !this.scheduledAt || this.scheduledAt.getTime() <= Date.now();
613
+ }
614
+ /**
615
+ * Get formatted hashtag string
616
+ */
617
+ get hashtagString() {
618
+ return this.hashtags.map((tag) => tag.startsWith("#") ? tag : `#${tag}`).join(" ");
619
+ }
620
+ /**
621
+ * Get the full post text with hashtags
622
+ */
623
+ get fullText() {
624
+ const parts = [this.description];
625
+ if (this.hashtags.length > 0) {
626
+ parts.push(this.hashtagString);
627
+ }
628
+ return parts.join("\n\n");
629
+ }
630
+ };
631
+ __decorateClass$1([
632
+ tenantId({ nullable: true })
633
+ ], SocialPost.prototype, "tenantId", 2);
634
+ __decorateClass$1([
635
+ foreignKey(() => SocialAccount)
636
+ ], SocialPost.prototype, "socialAccountId", 2);
637
+ __decorateClass$1([
638
+ foreignKey(() => VideoContent)
639
+ ], SocialPost.prototype, "videoContentId", 2);
640
+ __decorateClass$1([
641
+ crossPackageRef("@happyvertical/smrt-content:Content")
642
+ ], SocialPost.prototype, "contentId", 2);
643
+ SocialPost = __decorateClass$1([
644
+ TenantScoped({ mode: "optional" }),
645
+ smrt({
646
+ tableStrategy: "sti",
647
+ api: {
648
+ include: ["list", "get", "create", "update", "delete"]
649
+ },
650
+ mcp: {
651
+ include: ["list", "get"]
652
+ },
653
+ cli: true
654
+ })
655
+ ], SocialPost);
656
+ var __defProp = Object.defineProperty;
657
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
658
+ var __decorateClass = (decorators, target, key, kind) => {
659
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
660
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
661
+ if (decorator = decorators[i])
662
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
663
+ if (kind && result) __defProp(target, key, result);
664
+ return result;
665
+ };
666
+ let SocialPostAnalyticsSnapshot = class extends SmrtObject {
667
+ tenantId = null;
668
+ socialPostId = null;
669
+ platform = null;
670
+ analytics = {};
671
+ raw = null;
672
+ capturedAt = /* @__PURE__ */ new Date();
673
+ constructor(options = {}) {
674
+ super(options);
675
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
676
+ if (options.socialPostId !== void 0)
677
+ this.socialPostId = options.socialPostId;
678
+ if (options.platform !== void 0) this.platform = options.platform;
679
+ if (options.analytics !== void 0) this.analytics = options.analytics;
680
+ if (options.raw !== void 0) this.raw = options.raw;
681
+ if (options.capturedAt !== void 0) this.capturedAt = options.capturedAt;
682
+ }
683
+ };
684
+ __decorateClass([
685
+ tenantId({ nullable: true })
686
+ ], SocialPostAnalyticsSnapshot.prototype, "tenantId", 2);
687
+ __decorateClass([
688
+ foreignKey(() => SocialPost)
689
+ ], SocialPostAnalyticsSnapshot.prototype, "socialPostId", 2);
690
+ __decorateClass([
691
+ field({ required: true })
692
+ ], SocialPostAnalyticsSnapshot.prototype, "platform", 2);
693
+ __decorateClass([
694
+ field({ type: "json", nullable: true })
695
+ ], SocialPostAnalyticsSnapshot.prototype, "raw", 2);
696
+ SocialPostAnalyticsSnapshot = __decorateClass([
697
+ TenantScoped({ mode: "optional" }),
698
+ smrt({
699
+ tableStrategy: "sti",
700
+ api: {
701
+ include: ["list", "get", "create", "delete"]
702
+ },
703
+ mcp: {
704
+ include: ["list", "get"]
705
+ },
706
+ cli: true
707
+ })
708
+ ], SocialPostAnalyticsSnapshot);
709
+ function isRawAnalyticsPayload(value) {
710
+ return value === null || Array.isArray(value) || typeof value === "object" && value !== null;
711
+ }
712
+ class SocialPostAnalyticsSnapshotCollection extends SmrtCollection {
713
+ static _itemClass = SocialPostAnalyticsSnapshot;
714
+ async recordSnapshot(options) {
715
+ const snapshot = await this.create({
716
+ socialPostId: options.socialPostId,
717
+ platform: options.platform,
718
+ analytics: options.metrics,
719
+ raw: options.raw ?? (isRawAnalyticsPayload(options.metrics.raw) ? options.metrics.raw : null),
720
+ capturedAt: options.capturedAt ?? /* @__PURE__ */ new Date(),
721
+ tenantId: options.tenantId
722
+ });
723
+ return snapshot;
724
+ }
725
+ async findForPost(socialPostId) {
726
+ return this.list({
727
+ where: { socialPostId },
728
+ orderBy: "capturedAt DESC"
729
+ });
730
+ }
731
+ async findLatestForPost(socialPostId) {
732
+ const snapshots = await this.findForPost(socialPostId);
733
+ return snapshots[0] ?? null;
734
+ }
735
+ }
736
+ class SocialPostCollection extends SmrtCollection {
737
+ static _itemClass = SocialPost;
738
+ async createDraft(options) {
739
+ return this.create({
740
+ ...options,
741
+ postType: options.postType ?? (options.videoContentId || options.mediaUrl ? "video" : options.linkUrl ? "link" : "text"),
742
+ status: options.scheduledAt ? "scheduled" : "draft"
743
+ });
744
+ }
745
+ async findByStatus(status) {
746
+ return this.list({ where: { status }, orderBy: "scheduledAt ASC" });
747
+ }
748
+ async findForAccount(socialAccountId) {
749
+ return this.list({
750
+ where: { socialAccountId },
751
+ orderBy: "created_at DESC"
752
+ });
753
+ }
754
+ async findDueForPublish(now = /* @__PURE__ */ new Date()) {
755
+ const [approved, scheduled] = await Promise.all([
756
+ this.list({
757
+ where: { status: "approved" },
758
+ orderBy: "scheduledAt ASC"
759
+ }),
760
+ this.list({
761
+ where: { status: "scheduled", "scheduledAt <=": now },
762
+ orderBy: "scheduledAt ASC"
763
+ })
764
+ ]);
765
+ return [
766
+ ...approved.filter((post) => post.isDueForPublish),
767
+ ...scheduled
768
+ ].sort(
769
+ (a, b) => (a.scheduledAt?.getTime() ?? 0) - (b.scheduledAt?.getTime() ?? 0)
770
+ );
771
+ }
772
+ async recordPublishSuccess(post, data) {
773
+ const status = data.status ?? "published";
774
+ post.platformPostId = data.platformPostId;
775
+ post.platformUrl = data.platformUrl;
776
+ if (status === "published") {
777
+ post.publishedAt = data.publishedAt ?? post.publishedAt ?? /* @__PURE__ */ new Date();
778
+ }
779
+ post.status = status;
780
+ post.errorMessage = null;
781
+ await post.save();
782
+ return post;
783
+ }
784
+ async recordPublishFailure(post, error) {
785
+ post.status = "failed";
786
+ post.errorMessage = formatPublishError(error);
787
+ await post.save();
788
+ return post;
789
+ }
790
+ async updateLatestAnalytics(post, analytics, syncedAt = /* @__PURE__ */ new Date()) {
791
+ post.analytics = {
792
+ ...analytics,
793
+ lastUpdated: analytics.lastUpdated ?? syncedAt
794
+ };
795
+ post.analyticsLastSyncedAt = syncedAt;
796
+ await post.save();
797
+ return post;
798
+ }
799
+ }
800
+ function formatPublishError(error) {
801
+ if (typeof error === "string") {
802
+ return error;
803
+ }
804
+ const details = [error.message];
805
+ const code = error.code;
806
+ if (code !== void 0) {
807
+ details.push(`code=${String(code)}`);
808
+ }
809
+ if (error.stack) {
810
+ details.push(error.stack);
811
+ }
812
+ return details.join("\n");
813
+ }
814
+ export {
815
+ OAuthState,
816
+ OAuthStateCollection,
817
+ SocialAccount,
818
+ SocialAccountCollection,
819
+ SocialPost,
820
+ SocialPostAnalyticsSnapshot,
821
+ SocialPostAnalyticsSnapshotCollection,
822
+ SocialPostCollection
823
+ };
824
+ //# sourceMappingURL=index.js.map