@opencoredev/social-sdk 0.2.1 → 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 +87 -0
  37. package/dist/platforms/x.js +648 -120
  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
@@ -0,0 +1,15 @@
1
+ import type { PublishRequest } from "./core/types.js";
2
+ /**
3
+ * A JSON publish request the CLI cannot hand to `prepare`. The message names the
4
+ * offending path and never repeats the submitted value.
5
+ */
6
+ export declare class PublishRequestInputError extends Error {
7
+ readonly name = "PublishRequestInputError";
8
+ }
9
+ /**
10
+ * Parse and decode CLI input text into a `PublishRequest`. This checks the
11
+ * request's structure; `prepare` still reports semantic problems such as an
12
+ * unknown backend, an empty target list, or a past schedule as diagnostics.
13
+ * Text that is not JSON throws the `SyntaxError` from `JSON.parse`.
14
+ */
15
+ export declare function decodePublishRequest(text: string): PublishRequest;
@@ -0,0 +1,193 @@
1
+ import { definedFields } from "./core/fields.js";
2
+ import { isJsonValue } from "./transport/json.js";
3
+ import { isFiniteNumber, isJsonArray, isJsonObject, isString, } from "./transport/validation.js";
4
+ /**
5
+ * A JSON publish request the CLI cannot hand to `prepare`. The message names the
6
+ * offending path and never repeats the submitted value.
7
+ */
8
+ export class PublishRequestInputError extends Error {
9
+ name = "PublishRequestInputError";
10
+ }
11
+ /** JSON diagnostics deliberately accept only portable URLs, never executable streams or Blob handles. */
12
+ const httpsMediaOnlyMessage = "CLI media validation accepts HTTPS URL inputs only. Validate Blob/stream/media handles through the SDK.";
13
+ function fail(path, expectation) {
14
+ throw new PublishRequestInputError(`Invalid publish request: ${path} must be ${expectation}.`);
15
+ }
16
+ function objectAt(value, path) {
17
+ if (!isJsonObject(value))
18
+ fail(path, "an object");
19
+ return value;
20
+ }
21
+ function stringAt(value, path) {
22
+ if (!isString(value))
23
+ fail(path, "a string");
24
+ return value;
25
+ }
26
+ function optionalStringAt(value, path) {
27
+ return value === undefined ? undefined : stringAt(value, path);
28
+ }
29
+ function optionalNumberAt(value, path) {
30
+ if (value === undefined)
31
+ return undefined;
32
+ if (!isFiniteNumber(value))
33
+ fail(path, "a finite number");
34
+ return value;
35
+ }
36
+ function versionAt(value, path) {
37
+ if (value !== 1)
38
+ fail(path, "1");
39
+ return value;
40
+ }
41
+ function connectedAccount(value, path) {
42
+ const input = objectAt(value, path);
43
+ if (input["kind"] !== "connected-account")
44
+ fail(`${path}.kind`, '"connected-account"');
45
+ return {
46
+ kind: input["kind"],
47
+ version: versionAt(input["version"], `${path}.version`),
48
+ backend: stringAt(input["backend"], `${path}.backend`),
49
+ platform: stringAt(input["platform"], `${path}.platform`),
50
+ accountId: stringAt(input["accountId"], `${path}.accountId`),
51
+ };
52
+ }
53
+ function mediaRef(input, path) {
54
+ if (input["kind"] !== "media")
55
+ fail(`${path}.kind`, '"media"');
56
+ return {
57
+ kind: input["kind"],
58
+ version: versionAt(input["version"], `${path}.version`),
59
+ backend: stringAt(input["backend"], `${path}.backend`),
60
+ mediaId: stringAt(input["mediaId"], `${path}.mediaId`),
61
+ platform: stringAt(input["platform"], `${path}.platform`),
62
+ accountId: stringAt(input["accountId"], `${path}.accountId`),
63
+ };
64
+ }
65
+ function replyReference(value, path) {
66
+ const input = objectAt(value, path);
67
+ const kind = input["kind"];
68
+ const base = {
69
+ version: versionAt(input["version"], `${path}.version`),
70
+ backend: stringAt(input["backend"], `${path}.backend`),
71
+ platform: stringAt(input["platform"], `${path}.platform`),
72
+ accountId: stringAt(input["accountId"], `${path}.accountId`),
73
+ postId: stringAt(input["postId"], `${path}.postId`),
74
+ };
75
+ if (kind === "comment")
76
+ return { kind, ...base, commentId: stringAt(input["commentId"], `${path}.commentId`) };
77
+ if (kind !== "platform-post")
78
+ fail(`${path}.kind`, '"platform-post" or "comment"');
79
+ const native = input["native"];
80
+ return {
81
+ kind,
82
+ ...base,
83
+ ...definedFields({
84
+ native: native === undefined ? undefined : objectAt(native, `${path}.native`),
85
+ }),
86
+ };
87
+ }
88
+ function mediaSource(value, path) {
89
+ if (!isJsonObject(value) || value["kind"] !== "https-url")
90
+ throw new PublishRequestInputError(httpsMediaOnlyMessage);
91
+ return { kind: value["kind"], url: stringAt(value["url"], `${path}.url`) };
92
+ }
93
+ /** Thumbnails were never limited to URLs by the CLI, so JSON media references stay accepted. */
94
+ function thumbnailSource(value, path) {
95
+ const input = objectAt(value, path);
96
+ if (input["kind"] === "https-url")
97
+ return { kind: input["kind"], url: stringAt(input["url"], `${path}.url`) };
98
+ if (input["kind"] === "media-ref")
99
+ return {
100
+ kind: input["kind"],
101
+ ref: mediaRef(objectAt(input["ref"], `${path}.ref`), `${path}.ref`),
102
+ };
103
+ return fail(`${path}.kind`, '"https-url" or "media-ref"');
104
+ }
105
+ function mediaAttachment(value, path) {
106
+ const input = objectAt(value, path);
107
+ const kind = input["kind"];
108
+ if (kind !== "image" && kind !== "video")
109
+ fail(`${path}.kind`, '"image" or "video"');
110
+ const thumbnail = input["thumbnail"];
111
+ return {
112
+ kind,
113
+ source: mediaSource(input["source"], `${path}.source`),
114
+ ...definedFields({
115
+ mimeType: optionalStringAt(input["mimeType"], `${path}.mimeType`),
116
+ filename: optionalStringAt(input["filename"], `${path}.filename`),
117
+ byteSize: optionalNumberAt(input["byteSize"], `${path}.byteSize`),
118
+ width: optionalNumberAt(input["width"], `${path}.width`),
119
+ height: optionalNumberAt(input["height"], `${path}.height`),
120
+ durationSeconds: optionalNumberAt(input["durationSeconds"], `${path}.durationSeconds`),
121
+ altText: optionalStringAt(input["altText"], `${path}.altText`),
122
+ caption: optionalStringAt(input["caption"], `${path}.caption`),
123
+ thumbnail: thumbnail === undefined ? undefined : thumbnailSource(thumbnail, `${path}.thumbnail`),
124
+ }),
125
+ };
126
+ }
127
+ function link(value, path) {
128
+ const input = objectAt(value, path);
129
+ return {
130
+ url: stringAt(input["url"], `${path}.url`),
131
+ ...definedFields({
132
+ title: optionalStringAt(input["title"], `${path}.title`),
133
+ description: optionalStringAt(input["description"], `${path}.description`),
134
+ }),
135
+ };
136
+ }
137
+ function content(value, path) {
138
+ const input = objectAt(value, path);
139
+ const media = input["media"];
140
+ const linkInput = input["link"];
141
+ if (media !== undefined && !isJsonArray(media))
142
+ fail(`${path}.media`, "an array");
143
+ return definedFields({
144
+ text: optionalStringAt(input["text"], `${path}.text`),
145
+ media: media?.map((item, index) => mediaAttachment(item, `${path}.media[${index}]`)),
146
+ link: linkInput === undefined ? undefined : link(linkInput, `${path}.link`),
147
+ });
148
+ }
149
+ function target(value, path) {
150
+ const input = objectAt(value, path);
151
+ const override = input["content"];
152
+ const replyTo = input["replyTo"];
153
+ const options = input["options"];
154
+ return {
155
+ account: connectedAccount(input["account"], `${path}.account`),
156
+ ...definedFields({
157
+ content: override === undefined ? undefined : content(override, `${path}.content`),
158
+ replyTo: replyTo === undefined ? undefined : replyReference(replyTo, `${path}.replyTo`),
159
+ // Each adapter validates its own option fields during `prepare`.
160
+ options: options === undefined ? undefined : objectAt(options, `${path}.options`),
161
+ }),
162
+ };
163
+ }
164
+ function schedule(value, path) {
165
+ const input = objectAt(value, path);
166
+ return {
167
+ at: stringAt(input["at"], `${path}.at`),
168
+ ...definedFields({ timeZone: optionalStringAt(input["timeZone"], `${path}.timeZone`) }),
169
+ };
170
+ }
171
+ /**
172
+ * Parse and decode CLI input text into a `PublishRequest`. This checks the
173
+ * request's structure; `prepare` still reports semantic problems such as an
174
+ * unknown backend, an empty target list, or a past schedule as diagnostics.
175
+ * Text that is not JSON throws the `SyntaxError` from `JSON.parse`.
176
+ */
177
+ export function decodePublishRequest(text) {
178
+ const parsed = JSON.parse(text);
179
+ if (!isJsonValue(parsed) || !isJsonObject(parsed) || !isJsonArray(parsed["targets"]))
180
+ throw new PublishRequestInputError("Expected a JSON publish request with a targets array.");
181
+ const scheduleInput = parsed["schedule"];
182
+ const replyTo = parsed["replyTo"];
183
+ return {
184
+ targets: parsed["targets"].map((item, index) => target(item, `targets[${index}]`)),
185
+ content: content(parsed["content"], "content"),
186
+ ...definedFields({
187
+ idempotencyKey: optionalStringAt(parsed["idempotencyKey"], "idempotencyKey"),
188
+ correlationId: optionalStringAt(parsed["correlationId"], "correlationId"),
189
+ schedule: scheduleInput === undefined ? undefined : schedule(scheduleInput, "schedule"),
190
+ replyTo: replyTo === undefined ? undefined : replyReference(replyTo, "replyTo"),
191
+ }),
192
+ };
193
+ }
package/dist/cli.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import type { SocialAdapter } from "./core/index.js";
3
- declare const names: readonly ["mock", "zernio", "post-for-me", "bluesky", "x", "threads", "youtube", "tiktok", "instagram", "linkedin"];
4
- type AdapterName = (typeof names)[number];
3
+ /** Adapters the diagnostic CLI can construct offline. */
4
+ export declare const adapterNames: readonly ["mock", "zernio", "post-for-me", "bluesky", "x", "threads", "youtube", "tiktok", "instagram", "linkedin"];
5
+ export type AdapterName = (typeof adapterNames)[number];
6
+ export declare function isAdapterName(value: string): value is AdapterName;
5
7
  export declare function createDiagnosticAdapter(name: AdapterName, accountId?: string): SocialAdapter<unknown>;
6
8
  export interface CliIO {
7
9
  readonly env: Readonly<Record<string, string | undefined>>;
@@ -10,4 +12,3 @@ export interface CliIO {
10
12
  }
11
13
  /** All commands are offline and return 0 (valid), 1 (diagnostic failure), or 2 (usage/input error). */
12
14
  export declare function runCli(args: readonly string[], io: CliIO): Promise<number>;
13
- export {};
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- /* oxlint-disable anti-slop/no-runtime-typeof, anti-slop/no-unknown-parameters, anti-slop/no-unsafe-dictionary-type, anti-slop/require-safety-comment-for-type-assertion -- CLI JSON is parsed and validated at its input boundary. */
3
2
  import { realpathSync } from "node:fs";
4
3
  import { readFile, stat } from "node:fs/promises";
5
4
  import { pathToFileURL } from "node:url";
6
5
  import { createSocial } from "./core/client.js";
6
+ import { decodePublishRequest, PublishRequestInputError } from "./cli-request.js";
7
7
  import { mockBackend } from "./testing/index.js";
8
8
  import { zernio } from "./cloud/zernio.js";
9
9
  import { postForMe } from "./cloud/post-for-me.js";
@@ -14,7 +14,8 @@ import { youtube } from "./platforms/youtube.js";
14
14
  import { tiktok } from "./platforms/tiktok.js";
15
15
  import { instagram } from "./platforms/instagram.js";
16
16
  import { linkedin } from "./platforms/linkedin.js";
17
- const names = [
17
+ /** Adapters the diagnostic CLI can construct offline. */
18
+ export const adapterNames = [
18
19
  "mock",
19
20
  "zernio",
20
21
  "post-for-me",
@@ -26,6 +27,12 @@ const names = [
26
27
  "instagram",
27
28
  "linkedin",
28
29
  ];
30
+ export function isAdapterName(value) {
31
+ return adapterNames.some((name) => name === value);
32
+ }
33
+ function isLinkedInAuthorUrn(value) {
34
+ return value.startsWith("urn:li:person:") || value.startsWith("urn:li:organization:");
35
+ }
29
36
  const environmentNames = {
30
37
  mock: [],
31
38
  zernio: ["ZERNIO_API_KEY"],
@@ -83,9 +90,7 @@ export function createDiagnosticAdapter(name, accountId = "diagnostic-account")
83
90
  case "linkedin":
84
91
  return linkedin({
85
92
  auth: {
86
- author: accountId.startsWith("urn:li:")
87
- ? accountId
88
- : "urn:li:person:diagnostic",
93
+ author: isLinkedInAuthorUrn(accountId) ? accountId : "urn:li:person:diagnostic",
89
94
  accessToken: "offline-placeholder",
90
95
  },
91
96
  apiVersion: "202609",
@@ -97,6 +102,7 @@ export function createDiagnosticAdapter(name, accountId = "diagnostic-account")
97
102
  export async function runCli(args, io) {
98
103
  const command = args[0] ?? "help";
99
104
  const json = args.includes("--json");
105
+ // `data` is any report shape this command builds; it is only passed to JSON.stringify.
100
106
  const finish = (code, data) => {
101
107
  const result = { schemaVersion: 1, command, ok: code === 0, data };
102
108
  io.write(json ? JSON.stringify(result) + "\n" : JSON.stringify(result, null, 2) + "\n");
@@ -123,12 +129,12 @@ export async function runCli(args, io) {
123
129
  exitCodes: { success: 0, diagnosticFailure: 1, invalidInput: 2 },
124
130
  });
125
131
  const selected = options.get("--adapter") ?? "mock";
126
- if (!names.includes(selected))
127
- return finish(2, { error: "Unknown adapter.", adapters: names });
132
+ if (!isAdapterName(selected))
133
+ return finish(2, { error: "Unknown adapter.", adapters: adapterNames });
128
134
  const name = selected;
129
135
  if (command === "adapters")
130
136
  return finish(0, {
131
- adapters: names,
137
+ adapters: adapterNames,
132
138
  verification: "Local contract tests; no live checks are performed by this CLI.",
133
139
  });
134
140
  if (command === "doctor") {
@@ -176,18 +182,7 @@ export async function runCli(args, io) {
176
182
  const raw = await io.readInput(file);
177
183
  if (new TextEncoder().encode(raw).byteLength > 1024 * 1024)
178
184
  return finish(2, { error: "Input exceeds the 1 MiB diagnostic limit." });
179
- const request = JSON.parse(raw);
180
- if (!request ||
181
- typeof request !== "object" ||
182
- !Array.isArray(request["targets"]))
183
- return finish(2, { error: "Expected a JSON publish request with a targets array." });
184
- const input = request;
185
- // JSON diagnostics deliberately accept only portable URLs, never executable streams or Blob handles.
186
- const contents = [input.content, ...input.targets.map((target) => target.content)];
187
- if (contents.some((content) => content?.media?.some((media) => media.source?.kind !== "https-url")))
188
- return finish(2, {
189
- error: "CLI media validation accepts HTTPS URL inputs only. Validate Blob/stream/media handles through the SDK.",
190
- });
185
+ const input = decodePublishRequest(raw);
191
186
  const social = createSocial({
192
187
  backend: createDiagnosticAdapter(name, input.targets[0]?.account?.accountId),
193
188
  });
@@ -204,7 +199,10 @@ export async function runCli(args, io) {
204
199
  })),
205
200
  });
206
201
  }
207
- catch {
202
+ catch (error) {
203
+ // Decoder messages name a path or rule and never repeat submitted values.
204
+ if (error instanceof PublishRequestInputError)
205
+ return finish(2, { error: error.message });
208
206
  return finish(2, {
209
207
  error: "Unable to read or validate the JSON publish request. Check its shape and file permissions.",
210
208
  });
@@ -1,6 +1,7 @@
1
1
  import type { ManagedMediaStore } from "./media.js";
2
- import type { AdapterOperationContext, CapabilityManifest, ConnectedAccountRef, JsonObject, MediaAttachment, Platform, PreparationIssue, PreparedPublishTarget } from "../core/types.js";
2
+ import type { AdapterOperationContext, CapabilityManifest, ConnectedAccountRef, JsonObject, JsonValue, MediaAttachment, Platform, PreparationIssue, PreparedPublishTarget } from "../core/types.js";
3
3
  import { type HttpOptions } from "../transport/http.js";
4
+ import { type JsonField } from "../transport/validation.js";
4
5
  export interface ManagedOptions extends HttpOptions {
5
6
  readonly apiKey: string;
6
7
  readonly mediaStore?: ManagedMediaStore;
@@ -9,17 +10,17 @@ export interface ManagedOptions extends HttpOptions {
9
10
  readonly uploadHostAllowed?: (hostname: string) => boolean;
10
11
  }
11
12
  export declare const selectedPlatforms: readonly ["x", "threads", "bluesky", "youtube", "tiktok", "instagram", "linkedin", "facebook"];
12
- export declare function platform(value: unknown): Platform;
13
- export declare function managedHttp(origin: string, options: ManagedOptions): (path: string, context: AdapterOperationContext, body?: JsonObject, query?: Record<string, string>, method?: "GET" | "POST" | "PUT" | "DELETE") => Promise<unknown>;
13
+ export declare function platform(value: JsonField): Platform;
14
+ export declare function managedHttp(origin: string, options: ManagedOptions): (path: string, context: AdapterOperationContext, body?: JsonObject, query?: Record<string, string>, method?: "GET" | "POST" | "PUT" | "DELETE") => Promise<JsonValue>;
14
15
  export declare function capabilityManifest(backend: string, apiRevision: string, operations: readonly string[]): CapabilityManifest;
15
- export declare function optionsObject(target: PreparedPublishTarget): Record<string, unknown>;
16
+ export declare function optionsObject(target: PreparedPublishTarget): JsonObject;
16
17
  /** Every accepted normalized option has an intentional provider mapping. */
17
18
  export declare function managedOptionIssues(target: PreparedPublishTarget, provider: "zernio" | "post-for-me"): PreparationIssue[];
18
19
  export declare function managedPreparation(target: PreparedPublishTarget): PreparationIssue[];
19
- export declare function uploadManagedMedia(item: MediaAttachment, presign: (body: JsonObject) => Promise<unknown>, config: {
20
+ export declare function uploadManagedMedia(item: MediaAttachment, presign: (body: JsonObject) => Promise<JsonValue>, config: {
20
21
  options: ManagedOptions;
21
22
  provider: "zernio" | "post-for-me";
22
23
  context: AdapterOperationContext;
23
24
  }): Promise<string>;
24
25
  export declare function accountMatches(ref: Pick<ConnectedAccountRef, "backend" | "platform" | "accountId">, context: AdapterOperationContext): void;
25
- export declare function publicFields(value: unknown, fields: readonly string[]): JsonObject;
26
+ export declare function publicFields(value: JsonField, fields: readonly string[]): JsonObject;
@@ -1,8 +1,10 @@
1
1
  import { remainingBudget } from "../transport/budget.js";
2
2
  import { SocialError } from "../core/errors.js";
3
3
  import { createHttp, HttpError } from "../transport/http.js";
4
+ import { isJsonValue } from "../transport/json.js";
4
5
  import { httpsUrl, upload } from "../transport/upload.js";
5
- import { object, string } from "../transport/validation.js";
6
+ import { definedFields } from "../core/fields.js";
7
+ import { isBoolean, isJsonArray, isJsonObject, isString, object, string, } from "../transport/validation.js";
6
8
  export const selectedPlatforms = [
7
9
  "x",
8
10
  "threads",
@@ -13,11 +15,9 @@ export const selectedPlatforms = [
13
15
  "linkedin",
14
16
  "facebook",
15
17
  ];
16
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- provider payload is validated at this adapter boundary.
17
18
  export function platform(value) {
18
19
  const slug = value === "twitter" ? "x" : value;
19
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
20
- if (typeof slug !== "string" || !selectedPlatforms.some((item) => item === slug))
20
+ if (!isString(slug) || !selectedPlatforms.some((item) => item === slug))
21
21
  throw new SocialError({
22
22
  code: "unsupported_capability",
23
23
  operation: "accounts.read",
@@ -49,10 +49,10 @@ export function managedHttp(origin, options) {
49
49
  headers,
50
50
  timeoutMs: remainingBudget(context),
51
51
  method,
52
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
53
- ...(body === undefined ? {} : { body: JSON.stringify(body) }),
54
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
55
- ...(context.signal ? { signal: context.signal } : {}),
52
+ ...definedFields({
53
+ body: body === undefined ? undefined : JSON.stringify(body),
54
+ signal: context.signal,
55
+ }),
56
56
  maxAttempts: method === "GET" ? Math.min(5, context.retryBudget.maxAttempts) : 1,
57
57
  });
58
58
  }
@@ -88,8 +88,7 @@ export function managedHttp(origin, options) {
88
88
  backend: context.backendInstance,
89
89
  correlationId: context.correlationId,
90
90
  message: error.message,
91
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
92
- ...(error.status === undefined ? {} : { upstreamStatus: error.status }),
91
+ ...definedFields({ upstreamStatus: error.status }),
93
92
  retryDisposition: ambiguous
94
93
  ? { kind: "reconcile-first" }
95
94
  : error.status === 401
@@ -101,6 +100,13 @@ export function managedHttp(origin, options) {
101
100
  }
102
101
  };
103
102
  }
103
+ function publishFormats(platform) {
104
+ if (platform === "youtube")
105
+ return ["video"];
106
+ if (platform === "instagram" || platform === "tiktok")
107
+ return ["image", "video", "carousel"];
108
+ return ["text", "image", "video", "carousel"];
109
+ }
104
110
  export function capabilityManifest(backend, apiRevision, operations) {
105
111
  return {
106
112
  schemaVersion: 1,
@@ -111,29 +117,25 @@ export function capabilityManifest(backend, apiRevision, operations) {
111
117
  platform,
112
118
  operation,
113
119
  availability: "available",
114
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
115
- ...(operation === "posts.publish"
116
- ? {
117
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
118
- formats: (platform === "youtube"
119
- ? ["video"]
120
- : platform === "instagram" || platform === "tiktok"
121
- ? ["image", "video", "carousel"]
122
- : ["text", "image", "video", "carousel"]),
123
- }
124
- : {}),
120
+ ...definedFields({
121
+ formats: operation === "posts.publish" ? publishFormats(platform) : undefined,
122
+ }),
125
123
  notes: "Contract implementation; live account verification and provider/platform eligibility are separate.",
126
124
  }))),
127
125
  };
128
126
  }
129
- // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- provider payload is validated at this adapter boundary.
130
127
  export function optionsObject(target) {
131
- return target.options === undefined ? {} : object(target.options);
128
+ const options = target.options;
129
+ if (options === undefined)
130
+ return {};
131
+ // Reject exactly as `object` does, including options with members JSON cannot represent.
132
+ if (!isJsonValue(options))
133
+ throw new HttpError("Upstream response must be an object.", "invalid-response", true);
134
+ return object(options);
132
135
  }
133
136
  /** Every accepted normalized option has an intentional provider mapping. */
134
137
  export function managedOptionIssues(target, provider) {
135
138
  const config = optionsObject(target);
136
- // oxlint-disable-next-line anti-slop/no-known-value-widening -- provider payload is validated at this adapter boundary.
137
139
  const keys = {
138
140
  youtube: ["title", "visibility", "madeForKids"],
139
141
  instagram: ["shareToFeed"],
@@ -167,8 +169,7 @@ export function managedOptionIssues(target, provider) {
167
169
  "aiGenerated",
168
170
  "draft",
169
171
  ]) {
170
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
171
- if (config[key] !== undefined && typeof config[key] !== "boolean")
172
+ if (config[key] !== undefined && !isBoolean(config[key]))
172
173
  fail("options.boolean", "Consent, audience, interaction and disclosure choices must be booleans.");
173
174
  }
174
175
  if (config["replySettings"] !== undefined &&
@@ -184,8 +185,7 @@ export function managedOptionIssues(target, provider) {
184
185
  "aiGenerated",
185
186
  "draft",
186
187
  ])
187
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
188
- if (typeof config[key] !== "boolean")
188
+ if (!isBoolean(config[key]))
189
189
  fail("tiktok.explicit_choice", "Select every interaction, disclosure, AI-content and draft/direct-post choice before submission.");
190
190
  if (config["brandedContent"] === true && config["privacy"] === "SELF_ONLY")
191
191
  fail("tiktok.branded_privacy", "TikTok branded content cannot use private visibility.");
@@ -241,13 +241,11 @@ export function managedPreparation(target) {
241
241
  const options = optionsObject(target);
242
242
  if (media.length !== 1 || media[0]?.kind !== "video")
243
243
  fail("youtube.video", "YouTube requires exactly one video.");
244
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
245
- if (typeof options["title"] !== "string" || !options["title"])
244
+ if (!isString(options["title"]) || !options["title"])
246
245
  fail("youtube.title", "Select a YouTube title explicitly.");
247
246
  if (!["public", "unlisted", "private"].includes(String(options["visibility"])))
248
247
  fail("youtube.visibility", "Select public, unlisted, or private visibility explicitly.");
249
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
250
- if (typeof options["madeForKids"] !== "boolean")
248
+ if (!isBoolean(options["madeForKids"]))
251
249
  fail("youtube.audience", "Declare whether the video is made for kids.");
252
250
  }
253
251
  if (["instagram", "tiktok"].includes(target.account.platform) && media.length === 0)
@@ -261,9 +259,7 @@ export function managedPreparation(target) {
261
259
  }
262
260
  return issues;
263
261
  }
264
- export async function uploadManagedMedia(item,
265
- // oxlint-disable-next-line anti-slop/no-unknown-returns -- provider payload is validated at this adapter boundary.
266
- presign, config) {
262
+ export async function uploadManagedMedia(item, presign, config) {
267
263
  if (item.source.kind === "https-url")
268
264
  return httpsUrl(item.source.url).href;
269
265
  if (item.source.kind === "media-ref")
@@ -277,8 +273,7 @@ presign, config) {
277
273
  const source = item.source;
278
274
  const size = item.byteSize ?? (source.kind === "blob" ? source.blob.size : undefined);
279
275
  const data = object(await presign(config.provider === "zernio"
280
- ? // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
281
- { filename, contentType: mimeType, ...(size === undefined ? {} : { size }) }
276
+ ? { filename, contentType: mimeType, ...definedFields({ size }) }
282
277
  : {}));
283
278
  const uploadUrl = string(data[config.provider === "zernio" ? "uploadUrl" : "upload_url"]);
284
279
  const publicUrl = string(data[config.provider === "zernio" ? "publicUrl" : "media_url"]);
@@ -290,19 +285,13 @@ presign, config) {
290
285
  url: uploadUrl,
291
286
  source: {
292
287
  mimeType,
293
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
294
- ...(size === undefined ? {} : { size }),
295
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- Blob bodies preserve known-size uploads for presigned storage.
296
- ...(source.kind === "blob" ? { body: source.blob } : {}),
288
+ ...definedFields({ size, body: source.kind === "blob" ? source.blob : undefined }),
297
289
  open: source.kind === "blob" ? () => source.blob.stream() : source.open,
298
290
  },
299
291
  allowHost,
300
292
  maxBytes: 5 * 1024 * 1024 * 1024,
301
293
  timeoutMs: remainingBudget(config.context),
302
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
303
- ...(config.options.fetch ? { fetch: config.options.fetch } : {}),
304
- // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
305
- ...(config.context.signal ? { signal: config.context.signal } : {}),
294
+ ...definedFields({ fetch: config.options.fetch, signal: config.context.signal }),
306
295
  });
307
296
  return httpsUrl(publicUrl).href;
308
297
  }
@@ -314,20 +303,12 @@ export function accountMatches(ref, context) {
314
303
  message: "Account reference belongs to another backend instance.",
315
304
  });
316
305
  }
317
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- provider payload is validated at this adapter boundary.
318
306
  export function publicFields(value, fields) {
319
307
  const data = object(value);
320
308
  const result = {};
321
309
  for (const field of fields) {
322
310
  const value = data[field];
323
- if (
324
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
325
- typeof value === "string" ||
326
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
327
- typeof value === "boolean" ||
328
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
329
- typeof value === "number" ||
330
- value === null)
311
+ if (value !== undefined && !isJsonObject(value) && !isJsonArray(value))
331
312
  result[field] = value;
332
313
  }
333
314
  return result;