@opencoredev/social-sdk 0.3.0 → 0.5.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 (65) 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 +24 -21
  5. package/dist/cloud/common.d.ts +13 -8
  6. package/dist/cloud/common.js +40 -60
  7. package/dist/cloud/lifecycle.js +31 -35
  8. package/dist/cloud/media.d.ts +6 -2
  9. package/dist/cloud/media.js +50 -37
  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/postfast.d.ts +54 -0
  14. package/dist/cloud/postfast.js +578 -0
  15. package/dist/cloud/zernio.js +58 -98
  16. package/dist/core/client.js +79 -99
  17. package/dist/core/fields.d.ts +14 -0
  18. package/dist/core/fields.js +14 -0
  19. package/dist/core/idempotency.d.ts +7 -2
  20. package/dist/core/idempotency.js +37 -20
  21. package/dist/core/pagination.js +8 -7
  22. package/dist/core/types.d.ts +3 -2
  23. package/dist/platforms/bluesky.d.ts +65 -1
  24. package/dist/platforms/bluesky.js +675 -276
  25. package/dist/platforms/instagram.d.ts +2 -0
  26. package/dist/platforms/instagram.js +130 -105
  27. package/dist/platforms/linkedin.d.ts +58 -1
  28. package/dist/platforms/linkedin.js +877 -107
  29. package/dist/platforms/threads.d.ts +13 -1
  30. package/dist/platforms/threads.js +204 -302
  31. package/dist/platforms/tiktok.d.ts +4 -0
  32. package/dist/platforms/tiktok.js +140 -124
  33. package/dist/platforms/webhook-adapter.d.ts +9 -0
  34. package/dist/platforms/webhook-adapter.js +24 -0
  35. package/dist/platforms/x-engagement.js +7 -12
  36. package/dist/platforms/x-stream.d.ts +83 -0
  37. package/dist/platforms/x-stream.js +350 -0
  38. package/dist/platforms/x.d.ts +72 -0
  39. package/dist/platforms/x.js +328 -119
  40. package/dist/platforms/youtube-upload.d.ts +1 -1
  41. package/dist/platforms/youtube-upload.js +6 -2
  42. package/dist/platforms/youtube.d.ts +28 -4
  43. package/dist/platforms/youtube.js +291 -133
  44. package/dist/server/bluesky-oauth.d.ts +177 -0
  45. package/dist/server/bluesky-oauth.js +1229 -0
  46. package/dist/server/connections.d.ts +14 -0
  47. package/dist/server/connections.js +10 -2
  48. package/dist/server/egress.d.ts +14 -0
  49. package/dist/server/egress.js +115 -0
  50. package/dist/server/oauth-internal.d.ts +6 -0
  51. package/dist/server/oauth-internal.js +66 -0
  52. package/dist/server/oauth.d.ts +1 -1
  53. package/dist/server/oauth.js +46 -99
  54. package/dist/server/webhooks.d.ts +136 -3
  55. package/dist/server/webhooks.js +639 -25
  56. package/dist/testing/index.js +14 -28
  57. package/dist/transport/http.d.ts +1 -1
  58. package/dist/transport/http.js +0 -1
  59. package/dist/transport/json.d.ts +7 -0
  60. package/dist/transport/json.js +32 -4
  61. package/dist/transport/upload.d.ts +1 -1
  62. package/dist/transport/upload.js +46 -38
  63. package/dist/transport/validation.d.ts +16 -5
  64. package/dist/transport/validation.js +29 -7
  65. package/package.json +6 -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,9 +1,11 @@
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 {
5
5
  readonly ref: MediaRef;
6
6
  readonly publicUrl: string;
7
+ /** Storage key for providers that reference uploads by key rather than URL, such as PostFast. */
8
+ readonly providerKey?: string;
7
9
  readonly expiresAt?: string;
8
10
  readonly kind: "image" | "video";
9
11
  readonly mimeType: string;
@@ -17,7 +19,9 @@ export declare class MemoryManagedMediaStore implements ManagedMediaStore {
17
19
  put(record: ManagedMediaRecord): Promise<void>;
18
20
  get(mediaId: string): Promise<ManagedMediaRecord | undefined>;
19
21
  }
20
- export declare function managedMedia(provider: "zernio" | "post-for-me", options: ManagedOptions, presign: (body: JsonObject, context: AdapterOperationContext) => Promise<unknown>): {
22
+ /** Look up an uploaded asset and prove it belongs to this account and matches the attachment. */
23
+ export declare function storedMedia(item: MediaAttachment, ref: MediaRef, account: ConnectedAccountRef, store: ManagedMediaStore, options: Pick<ManagedOptions, "clock">): Promise<ManagedMediaRecord>;
24
+ export declare function managedMedia(provider: "zernio" | "post-for-me", options: ManagedOptions, presign: (body: JsonObject, context: AdapterOperationContext) => Promise<JsonValue>): {
21
25
  resolve: (item: MediaAttachment, account: ConnectedAccountRef, context: AdapterOperationContext) => Promise<string>;
22
26
  upload(item: MediaAttachment, account: ConnectedAccountRef, context: AdapterOperationContext): Promise<MediaRef>;
23
27
  };
@@ -10,57 +10,70 @@ 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
+ /** Look up an uploaded asset and prove it belongs to this account and matches the attachment. */
14
+ export async function storedMedia(item, ref, account, store, options) {
15
+ if (ref.backend !== account.backend ||
16
+ ref.accountId !== account.accountId ||
17
+ ref.platform !== account.platform)
18
+ throw new SocialError({
19
+ code: "unauthorized",
20
+ operation: "media.resolve",
21
+ message: "Media reference belongs to another account or backend.",
22
+ });
23
+ const record = await store.get(ref.mediaId);
24
+ if (!record ||
25
+ record.ref.backend !== ref.backend ||
26
+ record.ref.accountId !== ref.accountId ||
27
+ record.ref.platform !== ref.platform)
28
+ throw new SocialError({
29
+ code: "media_error",
30
+ operation: "media.resolve",
31
+ message: "Media reference is unknown to this server-side store.",
32
+ });
33
+ if (record.expiresAt &&
34
+ Date.parse(record.expiresAt) <= (options.clock?.() ?? new Date()).getTime())
35
+ throw new SocialError({
36
+ code: "media_error",
37
+ operation: "media.resolve",
38
+ message: "Stored media has expired. Upload a new asset explicitly.",
39
+ });
40
+ if (record.kind !== item.kind ||
41
+ (item.mimeType !== undefined && item.mimeType !== record.mimeType))
42
+ throw new SocialError({
43
+ code: "media_error",
44
+ operation: "media.resolve",
45
+ message: "Media kind or MIME type differs from the stored asset.",
46
+ });
47
+ return record;
48
+ }
49
+ export function managedMedia(provider, options, presign) {
16
50
  const store = options.mediaStore ?? new MemoryManagedMediaStore();
17
51
  async function resolve(item, account, context) {
18
52
  accountMatches(account, context);
53
+ if (item.kind === "document")
54
+ throw new SocialError({
55
+ code: "unsupported_capability",
56
+ operation: "media.resolve",
57
+ message: "Managed backends accept image and video media only through this adapter.",
58
+ });
19
59
  if (item.source.kind !== "media-ref")
20
60
  return uploadManagedMedia(item, (body) => presign(body, context), {
21
61
  options,
22
62
  context,
23
63
  provider,
24
64
  });
25
- const ref = item.source.ref;
26
- if (ref.backend !== account.backend ||
27
- ref.accountId !== account.accountId ||
28
- ref.platform !== account.platform)
29
- throw new SocialError({
30
- code: "unauthorized",
31
- operation: "media.resolve",
32
- message: "Media reference belongs to another account or backend.",
33
- });
34
- const record = await store.get(ref.mediaId);
35
- if (!record ||
36
- record.ref.backend !== ref.backend ||
37
- record.ref.accountId !== ref.accountId ||
38
- record.ref.platform !== ref.platform)
39
- throw new SocialError({
40
- code: "media_error",
41
- operation: "media.resolve",
42
- message: "Media reference is unknown to this server-side store.",
43
- });
44
- if (record.expiresAt &&
45
- Date.parse(record.expiresAt) <= (options.clock?.() ?? new Date()).getTime())
46
- throw new SocialError({
47
- code: "media_error",
48
- operation: "media.resolve",
49
- message: "Stored media has expired. Upload a new asset explicitly.",
50
- });
51
- if (record.kind !== item.kind ||
52
- (item.mimeType !== undefined && item.mimeType !== record.mimeType))
53
- throw new SocialError({
54
- code: "media_error",
55
- operation: "media.resolve",
56
- message: "Media kind or MIME type differs from the stored asset.",
57
- });
58
- return record.publicUrl;
65
+ return (await storedMedia(item, item.source.ref, account, store, options)).publicUrl;
59
66
  }
60
67
  return {
61
68
  resolve,
62
69
  async upload(item, account, context) {
63
70
  accountMatches(account, context);
71
+ if (item.kind === "document")
72
+ throw new SocialError({
73
+ code: "unsupported_capability",
74
+ operation: "media.upload",
75
+ message: "Managed backends accept image and video media only through this adapter.",
76
+ });
64
77
  if (item.source.kind === "media-ref") {
65
78
  await resolve(item, account, context);
66
79
  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
  },
@@ -0,0 +1,54 @@
1
+ export { MemoryManagedMediaStore, type ManagedMediaStore, type ManagedMediaRecord, } from "./media.js";
2
+ import type { AccountRecord, AdapterOperationContext, BackendPostRef, ConnectedAccountRef, DeliveryOutcome, MediaAttachment, MediaRef, MetricValue, PlatformPostRef, PreparationIssue, ScheduleCancellation, ScheduledJobRef } from "../core/types.js";
3
+ import { managedPreparation, type ManagedOptions } from "./common.js";
4
+ export interface PostFastConnectLinkOptions {
5
+ /** PostFast platform values such as `X` or `INSTAGRAM`. Omit to offer every platform. */
6
+ platforms?: readonly string[];
7
+ /** Days until the link expires. PostFast defaults to 7. */
8
+ expiryDays?: number;
9
+ redirectUrl?: string;
10
+ /** Your own identifier for the person connecting accounts. */
11
+ externalId?: string;
12
+ }
13
+ /**
14
+ * PostFast managed backend. PostFast only schedules posts, so every target needs a
15
+ * future `schedule.at`. Media is uploaded to PostFast storage and referenced by key.
16
+ */
17
+ export declare function postfast(options: ManagedOptions): {
18
+ id: string;
19
+ capabilities: import("../index.js").CapabilityManifest;
20
+ media: {
21
+ upload(item: MediaAttachment, target: ConnectedAccountRef, context: AdapterOperationContext): Promise<MediaRef>;
22
+ };
23
+ accounts: {
24
+ list(input: {
25
+ cursor?: string;
26
+ limit?: number;
27
+ }, context: AdapterOperationContext): Promise<{
28
+ nextCursor?: string;
29
+ items: AccountRecord[];
30
+ }>;
31
+ get(ref: ConnectedAccountRef, context: AdapterOperationContext): Promise<AccountRecord>;
32
+ };
33
+ posts: {
34
+ prepareTarget(target: Parameters<typeof managedPreparation>[0]): PreparationIssue[];
35
+ publishTarget(target: Parameters<typeof managedPreparation>[0], context: AdapterOperationContext): Promise<DeliveryOutcome>;
36
+ getDelivery(ref: {
37
+ deliveryId: string;
38
+ accountId: string;
39
+ platform: string;
40
+ backend: string;
41
+ }, context: AdapterOperationContext): Promise<DeliveryOutcome>;
42
+ cancelScheduled(ref: ScheduledJobRef, context: AdapterOperationContext): Promise<ScheduleCancellation>;
43
+ deleteBackendRecord(ref: BackendPostRef, context: AdapterOperationContext): Promise<void>;
44
+ };
45
+ analytics: {
46
+ getPostMetrics(ref: PlatformPostRef, context: AdapterOperationContext): Promise<readonly MetricValue[]>;
47
+ };
48
+ native: {
49
+ /** Create a hosted link where someone connects their social accounts to your workspace. */
50
+ createConnectLink(input: PostFastConnectLinkOptions, context: AdapterOperationContext): Promise<{
51
+ url: string;
52
+ }>;
53
+ };
54
+ };