@happyvertical/smrt-social 0.37.2 → 0.37.4

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