@opencoredev/social-sdk 0.3.0 → 0.4.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 (63) hide show
  1. package/dist/cli-request.d.ts +15 -0
  2. package/dist/cli-request.js +193 -0
  3. package/dist/cli.d.ts +4 -3
  4. package/dist/cli.js +19 -21
  5. package/dist/cloud/common.d.ts +7 -6
  6. package/dist/cloud/common.js +35 -54
  7. package/dist/cloud/lifecycle.js +31 -35
  8. package/dist/cloud/media.d.ts +2 -2
  9. package/dist/cloud/media.js +13 -3
  10. package/dist/cloud/outcomes.d.ts +4 -3
  11. package/dist/cloud/outcomes.js +8 -15
  12. package/dist/cloud/post-for-me.js +41 -49
  13. package/dist/cloud/zernio.js +58 -98
  14. package/dist/core/client.js +79 -99
  15. package/dist/core/fields.d.ts +14 -0
  16. package/dist/core/fields.js +14 -0
  17. package/dist/core/idempotency.d.ts +7 -2
  18. package/dist/core/idempotency.js +37 -20
  19. package/dist/core/pagination.js +8 -7
  20. package/dist/core/types.d.ts +3 -2
  21. package/dist/platforms/bluesky.d.ts +65 -1
  22. package/dist/platforms/bluesky.js +675 -276
  23. package/dist/platforms/instagram.d.ts +2 -0
  24. package/dist/platforms/instagram.js +130 -105
  25. package/dist/platforms/linkedin.d.ts +58 -1
  26. package/dist/platforms/linkedin.js +877 -107
  27. package/dist/platforms/threads.d.ts +13 -1
  28. package/dist/platforms/threads.js +204 -302
  29. package/dist/platforms/tiktok.d.ts +4 -0
  30. package/dist/platforms/tiktok.js +140 -124
  31. package/dist/platforms/webhook-adapter.d.ts +9 -0
  32. package/dist/platforms/webhook-adapter.js +24 -0
  33. package/dist/platforms/x-engagement.js +7 -12
  34. package/dist/platforms/x-stream.d.ts +83 -0
  35. package/dist/platforms/x-stream.js +350 -0
  36. package/dist/platforms/x.d.ts +72 -0
  37. package/dist/platforms/x.js +328 -119
  38. package/dist/platforms/youtube-upload.d.ts +1 -1
  39. package/dist/platforms/youtube-upload.js +6 -2
  40. package/dist/platforms/youtube.d.ts +28 -4
  41. package/dist/platforms/youtube.js +291 -133
  42. package/dist/server/bluesky-oauth.d.ts +177 -0
  43. package/dist/server/bluesky-oauth.js +1229 -0
  44. package/dist/server/connections.d.ts +14 -0
  45. package/dist/server/connections.js +10 -2
  46. package/dist/server/egress.d.ts +14 -0
  47. package/dist/server/egress.js +115 -0
  48. package/dist/server/oauth-internal.d.ts +6 -0
  49. package/dist/server/oauth-internal.js +66 -0
  50. package/dist/server/oauth.d.ts +1 -1
  51. package/dist/server/oauth.js +46 -99
  52. package/dist/server/webhooks.d.ts +136 -3
  53. package/dist/server/webhooks.js +639 -25
  54. package/dist/testing/index.js +14 -28
  55. package/dist/transport/http.d.ts +1 -1
  56. package/dist/transport/http.js +0 -1
  57. package/dist/transport/json.d.ts +7 -0
  58. package/dist/transport/json.js +32 -4
  59. package/dist/transport/upload.d.ts +1 -1
  60. package/dist/transport/upload.js +46 -38
  61. package/dist/transport/validation.d.ts +16 -5
  62. package/dist/transport/validation.js +29 -7
  63. package/package.json +2 -2
@@ -1,5 +1,6 @@
1
1
  import { SocialError } from "../core/errors.js";
2
- import { array, object, string } from "../transport/validation.js";
2
+ import { definedFields } from "../core/fields.js";
3
+ import { array, isString, object, string } from "../transport/validation.js";
3
4
  import { accountMatches } from "./common.js";
4
5
  const reject = (message) => {
5
6
  throw new SocialError({ code: "invalid_input", operation: "posts.lifecycle", message });
@@ -28,8 +29,7 @@ export function managedLifecycle(provider, request, now) {
28
29
  reject("This operation requires a backend record with exactly one destination.");
29
30
  const entry = entries[0];
30
31
  const rawId = provider === "zernio" ? entry["accountId"] : entry["id"];
31
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
32
- const accountId = typeof rawId === "string" ? rawId : object(rawId)["_id"];
32
+ const accountId = isString(rawId) ? rawId : object(rawId)["_id"];
33
33
  const platform = entry["platform"] === "twitter" ? "x" : entry["platform"];
34
34
  if (accountId !== ref.accountId || platform !== ref.platform)
35
35
  throw new SocialError({
@@ -46,13 +46,26 @@ export function managedLifecycle(provider, request, now) {
46
46
  : response["message"] !== "Post deleted successfully")
47
47
  unconfirmed();
48
48
  }
49
+ async function removeFromPlatform(ref, context) {
50
+ if (!["x", "threads", "bluesky", "youtube", "linkedin", "facebook"].includes(ref.platform))
51
+ reject("Zernio does not support native removal for this platform.");
52
+ const id = string(ref.native?.["backendRecordId"]);
53
+ const record = await owned(ref, id, context);
54
+ const entry = object(array(record["platforms"])[0]);
55
+ if (entry["status"] !== "published" || entry["platformPostId"] !== ref.postId)
56
+ reject("The backend record does not identify this published native post.");
57
+ const result = object(await request(`${path(id)}/unpublish`, context, {
58
+ platform: ref.platform === "x" ? "twitter" : ref.platform,
59
+ }));
60
+ if (result["success"] !== true)
61
+ unconfirmed();
62
+ }
49
63
  return {
50
64
  async cancelScheduled(ref, context) {
51
65
  const record = await owned(ref, ref.jobId, context);
52
66
  const scheduledAt = record[provider === "zernio" ? "scheduledFor" : "scheduled_at"];
53
67
  if (record["status"] !== "scheduled" ||
54
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
55
- typeof scheduledAt !== "string" ||
68
+ !isString(scheduledAt) ||
56
69
  !Number.isFinite(Date.parse(scheduledAt)) ||
57
70
  Date.parse(scheduledAt) <= Date.parse(now()))
58
71
  reject("Only a future scheduled record can be cancelled. Reconcile a due or dispatched post.");
@@ -61,23 +74,22 @@ export function managedLifecycle(provider, request, now) {
61
74
  return { state: "cancelled", backendRecord: "deleted" };
62
75
  }
63
76
  // A missing/null scheduled_at means publish immediately. Keep the timestamp.
64
- // oxlint-disable-next-line anti-slop/no-known-value-widening -- provider payload is validated at this adapter boundary.
77
+ const copied = [
78
+ "media",
79
+ "platform_configurations",
80
+ "account_configurations",
81
+ "external_id",
82
+ ].flatMap((key) => {
83
+ const value = record[key];
84
+ return value === undefined || value === null ? [] : [[key, value]];
85
+ });
65
86
  const body = {
66
87
  caption: string(record["caption"]),
67
88
  social_accounts: [ref.accountId],
68
89
  scheduled_at: string(scheduledAt),
69
90
  isDraft: true,
91
+ ...Object.fromEntries(copied),
70
92
  };
71
- for (const key of [
72
- "media",
73
- "platform_configurations",
74
- "account_configurations",
75
- "external_id",
76
- ]) {
77
- if (record[key] !== undefined && record[key] !== null)
78
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
79
- body[key] = record[key];
80
- }
81
93
  const result = object(await request(path(ref.jobId), context, body, {}, "PUT"));
82
94
  if (result["id"] !== ref.jobId || result["status"] !== "draft")
83
95
  unconfirmed();
@@ -89,24 +101,8 @@ export function managedLifecycle(provider, request, now) {
89
101
  reject("Delete only a draft backend record. Cancel a future schedule explicitly; never infer native deletion.");
90
102
  await deleteRecord(ref.recordId, context);
91
103
  },
92
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
93
- ...(provider === "zernio"
94
- ? {
95
- async removeFromPlatform(ref, context) {
96
- if (!["x", "threads", "bluesky", "youtube", "linkedin", "facebook"].includes(ref.platform))
97
- reject("Zernio does not support native removal for this platform.");
98
- const id = string(ref.native?.["backendRecordId"]);
99
- const record = await owned(ref, id, context);
100
- const entry = object(array(record["platforms"])[0]);
101
- if (entry["status"] !== "published" || entry["platformPostId"] !== ref.postId)
102
- reject("The backend record does not identify this published native post.");
103
- const result = object(await request(`${path(id)}/unpublish`, context, {
104
- platform: ref.platform === "x" ? "twitter" : ref.platform,
105
- }));
106
- if (result["success"] !== true)
107
- unconfirmed();
108
- },
109
- }
110
- : {}),
104
+ ...definedFields({
105
+ removeFromPlatform: provider === "zernio" ? removeFromPlatform : undefined,
106
+ }),
111
107
  };
112
108
  }
@@ -1,4 +1,4 @@
1
- import type { AdapterOperationContext, ConnectedAccountRef, JsonObject, MediaAttachment, MediaRef } from "../core/types.js";
1
+ import type { AdapterOperationContext, ConnectedAccountRef, JsonObject, JsonValue, MediaAttachment, MediaRef } from "../core/types.js";
2
2
  import { type ManagedOptions } from "./common.js";
3
3
  /** Keep this record server-side. A provider storage URL may grant access to the asset. */
4
4
  export interface ManagedMediaRecord {
@@ -17,7 +17,7 @@ export declare class MemoryManagedMediaStore implements ManagedMediaStore {
17
17
  put(record: ManagedMediaRecord): Promise<void>;
18
18
  get(mediaId: string): Promise<ManagedMediaRecord | undefined>;
19
19
  }
20
- export declare function managedMedia(provider: "zernio" | "post-for-me", options: ManagedOptions, presign: (body: JsonObject, context: AdapterOperationContext) => Promise<unknown>): {
20
+ export declare function managedMedia(provider: "zernio" | "post-for-me", options: ManagedOptions, presign: (body: JsonObject, context: AdapterOperationContext) => Promise<JsonValue>): {
21
21
  resolve: (item: MediaAttachment, account: ConnectedAccountRef, context: AdapterOperationContext) => Promise<string>;
22
22
  upload(item: MediaAttachment, account: ConnectedAccountRef, context: AdapterOperationContext): Promise<MediaRef>;
23
23
  };
@@ -10,12 +10,16 @@ export class MemoryManagedMediaStore {
10
10
  return record ? structuredClone(record) : undefined;
11
11
  }
12
12
  }
13
- export function managedMedia(provider, options,
14
- // oxlint-disable-next-line anti-slop/no-unknown-returns -- provider payload is validated at this adapter boundary.
15
- presign) {
13
+ export function managedMedia(provider, options, presign) {
16
14
  const store = options.mediaStore ?? new MemoryManagedMediaStore();
17
15
  async function resolve(item, account, context) {
18
16
  accountMatches(account, context);
17
+ if (item.kind === "document")
18
+ throw new SocialError({
19
+ code: "unsupported_capability",
20
+ operation: "media.resolve",
21
+ message: "Managed backends accept image and video media only through this adapter.",
22
+ });
19
23
  if (item.source.kind !== "media-ref")
20
24
  return uploadManagedMedia(item, (body) => presign(body, context), {
21
25
  options,
@@ -61,6 +65,12 @@ presign) {
61
65
  resolve,
62
66
  async upload(item, account, context) {
63
67
  accountMatches(account, context);
68
+ if (item.kind === "document")
69
+ throw new SocialError({
70
+ code: "unsupported_capability",
71
+ operation: "media.upload",
72
+ message: "Managed backends accept image and video media only through this adapter.",
73
+ });
64
74
  if (item.source.kind === "media-ref") {
65
75
  await resolve(item, account, context);
66
76
  return item.source.ref;
@@ -1,9 +1,10 @@
1
- import type { ConnectedAccountRef, DeliveryOutcome } from "../core/types.js";
1
+ import type { ConnectedAccountRef, DeliveryOutcome, JsonValue } from "../core/types.js";
2
+ import { type JsonField } from "../transport/validation.js";
2
3
  export interface OutcomeContext {
3
4
  account: ConnectedAccountRef;
4
5
  targetIndex: number;
5
6
  observedAt: string;
6
7
  }
7
8
  /** Never infer destination success from the aggregate HTTP/parent status. */
8
- export declare function zernioOutcome(value: unknown, context: OutcomeContext): DeliveryOutcome;
9
- export declare function postForMeOutcome(parentValue: unknown, resultsValue: unknown | undefined, context: OutcomeContext): DeliveryOutcome;
9
+ export declare function zernioOutcome(value: JsonValue, context: OutcomeContext): DeliveryOutcome;
10
+ export declare function postForMeOutcome(parentValue: JsonValue, resultsValue: JsonField, context: OutcomeContext): DeliveryOutcome;
@@ -1,4 +1,4 @@
1
- import { array, object, optionalString, string } from "../transport/validation.js";
1
+ import { array, isJsonArray, isJsonObject, isString, object, optionalString, string, } from "../transport/validation.js";
2
2
  function delivery(context, deliveryId) {
3
3
  return {
4
4
  kind: "delivery",
@@ -10,7 +10,6 @@ function delivery(context, deliveryId) {
10
10
  };
11
11
  }
12
12
  /** Never infer destination success from the aggregate HTTP/parent status. */
13
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- provider payload is validated at this adapter boundary.
14
13
  export function zernioOutcome(value, context) {
15
14
  const response = object(value);
16
15
  const post = object(response["post"] ?? response["existingPost"] ?? response);
@@ -18,14 +17,12 @@ export function zernioOutcome(value, context) {
18
17
  const platform = context.account.platform === "x" ? "twitter" : context.account.platform;
19
18
  const entries = array(post["platforms"]).map(object);
20
19
  const matches = entries.filter((entry) => {
21
- const accountId =
22
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
23
- typeof entry["accountId"] === "string"
24
- ? entry["accountId"]
25
- : // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
26
- entry["accountId"] && typeof entry["accountId"] === "object"
27
- ? optionalString(object(entry["accountId"])["_id"])
28
- : undefined;
20
+ const rawAccountId = entry["accountId"];
21
+ const accountId = isString(rawAccountId)
22
+ ? rawAccountId
23
+ : isJsonObject(rawAccountId) || isJsonArray(rawAccountId)
24
+ ? optionalString(object(rawAccountId)["_id"])
25
+ : undefined;
29
26
  return entry["platform"] === platform && accountId === context.account.accountId;
30
27
  });
31
28
  const base = { ...context, delivery: delivery(context, postId) };
@@ -104,11 +101,7 @@ export function zernioOutcome(value, context) {
104
101
  };
105
102
  }
106
103
  }
107
- export function postForMeOutcome(
108
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- provider payload is validated at this adapter boundary.
109
- parentValue,
110
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- provider payload is validated at this adapter boundary.
111
- resultsValue, context) {
104
+ export function postForMeOutcome(parentValue, resultsValue, context) {
112
105
  const parent = object(parentValue);
113
106
  const postId = string(parent["id"]);
114
107
  const backendState = optionalString(parent["status"]) ?? "unknown";
@@ -1,10 +1,10 @@
1
- /* oxlint-disable anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract. */
2
1
  import { managedLifecycle } from "./lifecycle.js";
3
2
  import { managedMedia } from "./media.js";
4
3
  export { MemoryManagedMediaStore, } from "./media.js";
5
4
  import { defineAdapter } from "../core/adapter.js";
6
5
  import { SocialError } from "../core/errors.js";
7
- import { array, object, optionalNumber, optionalString, string } from "../transport/validation.js";
6
+ import { definedFields } from "../core/fields.js";
7
+ import { array, isBoolean, object, optionalNumber, optionalString, string, } from "../transport/validation.js";
8
8
  import { verifyPostForMeWebhook, decodeWebhook } from "../server/webhooks.js";
9
9
  import { accountMatches, capabilityManifest, managedHttp, managedPreparation, managedOptionIssues, optionsObject, platform, publicFields, } from "./common.js";
10
10
  import { postForMeOutcome } from "./outcomes.js";
@@ -12,7 +12,6 @@ export function postForMe(options) {
12
12
  const request = managedHttp("https://api.postforme.dev", options);
13
13
  const mediaPipeline = managedMedia("post-for-me", options, (body, context) => request("/v1/media/create-upload-url", context, body));
14
14
  const now = () => (options.clock?.() ?? new Date()).toISOString();
15
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- provider payload is validated at this adapter boundary.
16
15
  const account = (value, backend) => {
17
16
  const row = object(value);
18
17
  const handle = optionalString(row["username"]);
@@ -25,8 +24,7 @@ export function postForMe(options) {
25
24
  accountId: string(row["id"]),
26
25
  },
27
26
  displayName: handle ?? string(row["user_id"]),
28
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
29
- ...(handle ? { handle } : {}),
27
+ ...definedFields({ handle: handle || undefined }),
30
28
  status: row["status"] === "connected"
31
29
  ? "connected"
32
30
  : row["status"] === "disconnected"
@@ -34,7 +32,6 @@ export function postForMe(options) {
34
32
  : "unknown",
35
33
  };
36
34
  };
37
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- validated by account at this boundary.
38
35
  const supportedAccount = (value, backend) => {
39
36
  try {
40
37
  return account(value, backend);
@@ -47,9 +44,11 @@ export function postForMe(options) {
47
44
  };
48
45
  const feed = async (ref, context, metrics) => {
49
46
  accountMatches(ref, context);
50
- const result = object(await request(`/v1/social-account-feeds/${encodeURIComponent(ref.accountId)}`, context, undefined,
51
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
52
- { platform_post_id: ref.postId, limit: "1", ...(metrics ? { expand: "metrics" } : {}) }));
47
+ const result = object(await request(`/v1/social-account-feeds/${encodeURIComponent(ref.accountId)}`, context, undefined, {
48
+ platform_post_id: ref.postId,
49
+ limit: "1",
50
+ ...definedFields({ expand: metrics ? "metrics" : undefined }),
51
+ }));
53
52
  const row = array(result["data"])
54
53
  .map(object)
55
54
  .find((item) => item["platform_post_id"] === ref.postId && item["social_account_id"] === ref.accountId);
@@ -102,8 +101,7 @@ export function postForMe(options) {
102
101
  const parsed = supportedAccount(value, context.backendInstance);
103
102
  return parsed ? [parsed] : [];
104
103
  }),
105
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
106
- ...(next ? { nextCursor: next } : {}),
104
+ ...definedFields({ nextCursor: next }),
107
105
  };
108
106
  },
109
107
  async get(ref, context) {
@@ -131,8 +129,7 @@ export function postForMe(options) {
131
129
  });
132
130
  const result = object(await request(`/v1/social-account-feeds/${encodeURIComponent(account.accountId)}`, context, undefined, {
133
131
  limit: String(limit),
134
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
135
- ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
132
+ ...definedFields({ cursor: input.cursor }),
136
133
  }));
137
134
  const rows = array(result["data"])
138
135
  .map(object)
@@ -159,14 +156,7 @@ export function postForMe(options) {
159
156
  : optionalString(meta["next"]) && cursor
160
157
  ? cursor
161
158
  : undefined;
162
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
163
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
164
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
165
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
166
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
167
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract.
168
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
169
- return { items: rows, ...(next === undefined ? {} : { nextCursor: next }) };
159
+ return { items: rows, ...definedFields({ nextCursor: next }) };
170
160
  },
171
161
  prepareTarget(target) {
172
162
  const issues = [
@@ -200,9 +190,9 @@ export function postForMe(options) {
200
190
  media.push({ url: await mediaPipeline.resolve(item, target.account, context) });
201
191
  const config = optionsObject(target);
202
192
  const platformConfig = {};
203
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
204
- if (target.account.platform === "instagram" && typeof config["shareToFeed"] === "boolean")
205
- platformConfig["share_to_feed"] = config["shareToFeed"];
193
+ const shareToFeed = config["shareToFeed"];
194
+ if (target.account.platform === "instagram" && isBoolean(shareToFeed))
195
+ platformConfig["share_to_feed"] = shareToFeed;
206
196
  if (target.account.platform === "x" &&
207
197
  config["replySettings"] !== undefined &&
208
198
  config["replySettings"] !== "everyone")
@@ -210,9 +200,9 @@ export function postForMe(options) {
210
200
  if (target.account.platform === "youtube") {
211
201
  platformConfig["title"] = string(config["title"]);
212
202
  platformConfig["privacy_status"] = string(config["visibility"]);
213
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
214
- if (typeof config["madeForKids"] === "boolean")
215
- platformConfig["made_for_kids"] = config["madeForKids"];
203
+ const madeForKids = config["madeForKids"];
204
+ if (isBoolean(madeForKids))
205
+ platformConfig["made_for_kids"] = madeForKids;
216
206
  }
217
207
  if (target.account.platform === "tiktok") {
218
208
  platformConfig["privacy_status"] =
@@ -220,31 +210,34 @@ export function postForMe(options) {
220
210
  platformConfig["auto_add_music"] = false;
221
211
  platformConfig["allow_duet"] = !config["disableDuet"];
222
212
  platformConfig["allow_stitch"] = !config["disableStitch"];
223
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
224
- platformConfig["disclose_your_brand"] = config["ownBrand"];
225
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
226
- platformConfig["is_ai_generated"] = config["aiGenerated"];
227
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
228
- platformConfig["is_draft"] = config["draft"];
229
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
230
- if (typeof config["disableComments"] === "boolean")
231
- platformConfig["allow_comment"] = !config["disableComments"];
232
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
233
- if (typeof config["brandedContent"] === "boolean")
234
- platformConfig["disclose_branded_content"] = config["brandedContent"];
213
+ // prepareTarget requires these TikTok choices to be booleans. Skipping an
214
+ // absent value sends the same JSON body, since serialization drops undefined.
215
+ for (const [nativeKey, optionKey] of [
216
+ ["disclose_your_brand", "ownBrand"],
217
+ ["is_ai_generated", "aiGenerated"],
218
+ ["is_draft", "draft"],
219
+ ]) {
220
+ const value = config[optionKey];
221
+ if (value !== undefined)
222
+ platformConfig[nativeKey] = value;
223
+ }
224
+ const { disableComments, brandedContent } = config;
225
+ if (isBoolean(disableComments))
226
+ platformConfig["allow_comment"] = !disableComments;
227
+ if (isBoolean(brandedContent))
228
+ platformConfig["disclose_branded_content"] = brandedContent;
235
229
  }
236
230
  const response = await request("/v1/social-posts", context, {
237
231
  caption: target.content.text ?? "",
238
232
  social_accounts: [target.account.accountId],
239
233
  media,
240
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
241
- ...(target.schedule ? { scheduled_at: target.schedule.at } : {}),
242
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
243
- ...(context.targetIdempotencyKey ? { external_id: context.targetIdempotencyKey } : {}),
244
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
245
- ...(Object.keys(platformConfig).length
246
- ? { platform_configurations: { [target.account.platform]: platformConfig } }
247
- : {}),
234
+ ...definedFields({
235
+ scheduled_at: target.schedule?.at,
236
+ external_id: context.targetIdempotencyKey || undefined,
237
+ platform_configurations: Object.keys(platformConfig).length
238
+ ? { [target.account.platform]: platformConfig }
239
+ : undefined,
240
+ }),
248
241
  });
249
242
  const parent = object(response);
250
243
  let results;
@@ -385,8 +378,7 @@ export function postForMe(options) {
385
378
  platform: input.platform,
386
379
  external_id: input.externalId,
387
380
  permissions: input.permissions,
388
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
389
- ...(input.redirectUrl ? { redirect_url_override: input.redirectUrl } : {}),
381
+ ...definedFields({ redirect_url_override: input.redirectUrl || undefined }),
390
382
  }));
391
383
  return { url: string(result["url"]), platform: string(result["platform"]) };
392
384
  },