@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
@@ -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", "postfast", "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,12 +1,13 @@
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";
10
+ import { postfast } from "./cloud/postfast.js";
10
11
  import { bluesky } from "./platforms/bluesky.js";
11
12
  import { x } from "./platforms/x.js";
12
13
  import { threads } from "./platforms/threads.js";
@@ -14,10 +15,12 @@ import { youtube } from "./platforms/youtube.js";
14
15
  import { tiktok } from "./platforms/tiktok.js";
15
16
  import { instagram } from "./platforms/instagram.js";
16
17
  import { linkedin } from "./platforms/linkedin.js";
17
- const names = [
18
+ /** Adapters the diagnostic CLI can construct offline. */
19
+ export const adapterNames = [
18
20
  "mock",
19
21
  "zernio",
20
22
  "post-for-me",
23
+ "postfast",
21
24
  "bluesky",
22
25
  "x",
23
26
  "threads",
@@ -26,10 +29,17 @@ const names = [
26
29
  "instagram",
27
30
  "linkedin",
28
31
  ];
32
+ export function isAdapterName(value) {
33
+ return adapterNames.some((name) => name === value);
34
+ }
35
+ function isLinkedInAuthorUrn(value) {
36
+ return value.startsWith("urn:li:person:") || value.startsWith("urn:li:organization:");
37
+ }
29
38
  const environmentNames = {
30
39
  mock: [],
31
40
  zernio: ["ZERNIO_API_KEY"],
32
41
  "post-for-me": ["POST_FOR_ME_API_KEY"],
42
+ postfast: ["POSTFAST_API_KEY"],
33
43
  bluesky: ["BLUESKY_SERVICE", "BLUESKY_DID", "BLUESKY_ACCESS_JWT"],
34
44
  x: ["X_USER_ID", "X_ACCESS_TOKEN"],
35
45
  threads: ["THREADS_USER_ID", "THREADS_ACCESS_TOKEN"],
@@ -49,6 +59,8 @@ export function createDiagnosticAdapter(name, accountId = "diagnostic-account")
49
59
  return zernio({ apiKey: "offline-placeholder", fetch: noNetwork });
50
60
  case "post-for-me":
51
61
  return postForMe({ apiKey: "offline-placeholder", fetch: noNetwork });
62
+ case "postfast":
63
+ return postfast({ apiKey: "offline-placeholder", fetch: noNetwork });
52
64
  case "bluesky":
53
65
  return bluesky({
54
66
  auth: { service: "https://bsky.social", did: accountId, accessJwt: "offline-placeholder" },
@@ -83,9 +95,7 @@ export function createDiagnosticAdapter(name, accountId = "diagnostic-account")
83
95
  case "linkedin":
84
96
  return linkedin({
85
97
  auth: {
86
- author: accountId.startsWith("urn:li:")
87
- ? accountId
88
- : "urn:li:person:diagnostic",
98
+ author: isLinkedInAuthorUrn(accountId) ? accountId : "urn:li:person:diagnostic",
89
99
  accessToken: "offline-placeholder",
90
100
  },
91
101
  apiVersion: "202609",
@@ -97,6 +107,7 @@ export function createDiagnosticAdapter(name, accountId = "diagnostic-account")
97
107
  export async function runCli(args, io) {
98
108
  const command = args[0] ?? "help";
99
109
  const json = args.includes("--json");
110
+ // `data` is any report shape this command builds; it is only passed to JSON.stringify.
100
111
  const finish = (code, data) => {
101
112
  const result = { schemaVersion: 1, command, ok: code === 0, data };
102
113
  io.write(json ? JSON.stringify(result) + "\n" : JSON.stringify(result, null, 2) + "\n");
@@ -123,12 +134,12 @@ export async function runCli(args, io) {
123
134
  exitCodes: { success: 0, diagnosticFailure: 1, invalidInput: 2 },
124
135
  });
125
136
  const selected = options.get("--adapter") ?? "mock";
126
- if (!names.includes(selected))
127
- return finish(2, { error: "Unknown adapter.", adapters: names });
137
+ if (!isAdapterName(selected))
138
+ return finish(2, { error: "Unknown adapter.", adapters: adapterNames });
128
139
  const name = selected;
129
140
  if (command === "adapters")
130
141
  return finish(0, {
131
- adapters: names,
142
+ adapters: adapterNames,
132
143
  verification: "Local contract tests; no live checks are performed by this CLI.",
133
144
  });
134
145
  if (command === "doctor") {
@@ -176,18 +187,7 @@ export async function runCli(args, io) {
176
187
  const raw = await io.readInput(file);
177
188
  if (new TextEncoder().encode(raw).byteLength > 1024 * 1024)
178
189
  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
- });
190
+ const input = decodePublishRequest(raw);
191
191
  const social = createSocial({
192
192
  backend: createDiagnosticAdapter(name, input.targets[0]?.account?.accountId),
193
193
  });
@@ -204,7 +204,10 @@ export async function runCli(args, io) {
204
204
  })),
205
205
  });
206
206
  }
207
- catch {
207
+ catch (error) {
208
+ // Decoder messages name a path or rule and never repeat submitted values.
209
+ if (error instanceof PublishRequestInputError)
210
+ return finish(2, { error: error.message });
208
211
  return finish(2, {
209
212
  error: "Unable to read or validate the JSON publish request. Check its shape and file permissions.",
210
213
  });
@@ -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, CapabilityDeclaration, 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,21 @@ 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>;
14
- export declare function capabilityManifest(backend: string, apiRevision: string, operations: readonly string[]): CapabilityManifest;
15
- export declare function optionsObject(target: PreparedPublishTarget): Record<string, unknown>;
13
+ export declare function platform(value: JsonField): Platform;
14
+ /** How a provider expects its server-side API key. Defaults to a bearer token. */
15
+ export type ManagedAuthHeader = (apiKey: string) => readonly [name: string, value: string];
16
+ export declare function managedHttp(origin: string, options: ManagedOptions, authHeader?: ManagedAuthHeader): (path: string, context: AdapterOperationContext, body?: JsonObject, query?: Record<string, string>, method?: "GET" | "POST" | "PUT" | "DELETE") => Promise<JsonValue>;
17
+ export type PublishFormats = (platform: (typeof selectedPlatforms)[number]) => CapabilityDeclaration["formats"];
18
+ export declare const publishFormats: PublishFormats;
19
+ export declare function capabilityManifest(backend: string, apiRevision: string, operations: readonly string[], formats?: PublishFormats): CapabilityManifest;
20
+ export declare function optionsObject(target: PreparedPublishTarget): JsonObject;
16
21
  /** Every accepted normalized option has an intentional provider mapping. */
17
- export declare function managedOptionIssues(target: PreparedPublishTarget, provider: "zernio" | "post-for-me"): PreparationIssue[];
22
+ export declare function managedOptionIssues(target: PreparedPublishTarget, provider: "zernio" | "post-for-me" | "postfast"): PreparationIssue[];
18
23
  export declare function managedPreparation(target: PreparedPublishTarget): PreparationIssue[];
19
- export declare function uploadManagedMedia(item: MediaAttachment, presign: (body: JsonObject) => Promise<unknown>, config: {
24
+ export declare function uploadManagedMedia(item: MediaAttachment, presign: (body: JsonObject) => Promise<JsonValue>, config: {
20
25
  options: ManagedOptions;
21
26
  provider: "zernio" | "post-for-me";
22
27
  context: AdapterOperationContext;
23
28
  }): Promise<string>;
24
29
  export declare function accountMatches(ref: Pick<ConnectedAccountRef, "backend" | "platform" | "accountId">, context: AdapterOperationContext): void;
25
- export declare function publicFields(value: unknown, fields: readonly string[]): JsonObject;
30
+ 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",
@@ -25,7 +25,8 @@ export function platform(value) {
25
25
  });
26
26
  return slug;
27
27
  }
28
- export function managedHttp(origin, options) {
28
+ const bearer = (apiKey) => ["Authorization", `Bearer ${apiKey}`];
29
+ export function managedHttp(origin, options, authHeader = bearer) {
29
30
  if (!options.apiKey.trim())
30
31
  throw new SocialError({
31
32
  code: "invalid_config",
@@ -37,10 +38,8 @@ export function managedHttp(origin, options) {
37
38
  const url = new URL(origin + path);
38
39
  for (const [key, value] of Object.entries(query))
39
40
  url.searchParams.set(key, value);
40
- const headers = new Headers({
41
- Authorization: `Bearer ${options.apiKey}`,
42
- "Content-Type": "application/json",
43
- });
41
+ const headers = new Headers({ "Content-Type": "application/json" });
42
+ headers.set(...authHeader(options.apiKey));
44
43
  if (context.targetIdempotencyKey && origin.includes("zernio.com"))
45
44
  headers.set(path === "/v1/posts" ? "x-request-id" : "Idempotency-Key", context.targetIdempotencyKey);
46
45
  try {
@@ -49,10 +48,10 @@ export function managedHttp(origin, options) {
49
48
  headers,
50
49
  timeoutMs: remainingBudget(context),
51
50
  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 } : {}),
51
+ ...definedFields({
52
+ body: body === undefined ? undefined : JSON.stringify(body),
53
+ signal: context.signal,
54
+ }),
56
55
  maxAttempts: method === "GET" ? Math.min(5, context.retryBudget.maxAttempts) : 1,
57
56
  });
58
57
  }
@@ -88,8 +87,7 @@ export function managedHttp(origin, options) {
88
87
  backend: context.backendInstance,
89
88
  correlationId: context.correlationId,
90
89
  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 }),
90
+ ...definedFields({ upstreamStatus: error.status }),
93
91
  retryDisposition: ambiguous
94
92
  ? { kind: "reconcile-first" }
95
93
  : error.status === 401
@@ -101,7 +99,14 @@ export function managedHttp(origin, options) {
101
99
  }
102
100
  };
103
101
  }
104
- export function capabilityManifest(backend, apiRevision, operations) {
102
+ export const publishFormats = (platform) => {
103
+ if (platform === "youtube")
104
+ return ["video"];
105
+ if (platform === "instagram" || platform === "tiktok")
106
+ return ["image", "video", "carousel"];
107
+ return ["text", "image", "video", "carousel"];
108
+ };
109
+ export function capabilityManifest(backend, apiRevision, operations, formats = publishFormats) {
105
110
  return {
106
111
  schemaVersion: 1,
107
112
  backend,
@@ -111,29 +116,25 @@ export function capabilityManifest(backend, apiRevision, operations) {
111
116
  platform,
112
117
  operation,
113
118
  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
- : {}),
119
+ ...definedFields({
120
+ formats: operation === "posts.publish" ? formats(platform) : undefined,
121
+ }),
125
122
  notes: "Contract implementation; live account verification and provider/platform eligibility are separate.",
126
123
  }))),
127
124
  };
128
125
  }
129
- // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- provider payload is validated at this adapter boundary.
130
126
  export function optionsObject(target) {
131
- return target.options === undefined ? {} : object(target.options);
127
+ const options = target.options;
128
+ if (options === undefined)
129
+ return {};
130
+ // Reject exactly as `object` does, including options with members JSON cannot represent.
131
+ if (!isJsonValue(options))
132
+ throw new HttpError("Upstream response must be an object.", "invalid-response", true);
133
+ return object(options);
132
134
  }
133
135
  /** Every accepted normalized option has an intentional provider mapping. */
134
136
  export function managedOptionIssues(target, provider) {
135
137
  const config = optionsObject(target);
136
- // oxlint-disable-next-line anti-slop/no-known-value-widening -- provider payload is validated at this adapter boundary.
137
138
  const keys = {
138
139
  youtube: ["title", "visibility", "madeForKids"],
139
140
  instagram: ["shareToFeed"],
@@ -167,8 +168,7 @@ export function managedOptionIssues(target, provider) {
167
168
  "aiGenerated",
168
169
  "draft",
169
170
  ]) {
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")
171
+ if (config[key] !== undefined && !isBoolean(config[key]))
172
172
  fail("options.boolean", "Consent, audience, interaction and disclosure choices must be booleans.");
173
173
  }
174
174
  if (config["replySettings"] !== undefined &&
@@ -184,8 +184,7 @@ export function managedOptionIssues(target, provider) {
184
184
  "aiGenerated",
185
185
  "draft",
186
186
  ])
187
- // oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
188
- if (typeof config[key] !== "boolean")
187
+ if (!isBoolean(config[key]))
189
188
  fail("tiktok.explicit_choice", "Select every interaction, disclosure, AI-content and draft/direct-post choice before submission.");
190
189
  if (config["brandedContent"] === true && config["privacy"] === "SELF_ONLY")
191
190
  fail("tiktok.branded_privacy", "TikTok branded content cannot use private visibility.");
@@ -241,13 +240,11 @@ export function managedPreparation(target) {
241
240
  const options = optionsObject(target);
242
241
  if (media.length !== 1 || media[0]?.kind !== "video")
243
242
  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"])
243
+ if (!isString(options["title"]) || !options["title"])
246
244
  fail("youtube.title", "Select a YouTube title explicitly.");
247
245
  if (!["public", "unlisted", "private"].includes(String(options["visibility"])))
248
246
  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")
247
+ if (!isBoolean(options["madeForKids"]))
251
248
  fail("youtube.audience", "Declare whether the video is made for kids.");
252
249
  }
253
250
  if (["instagram", "tiktok"].includes(target.account.platform) && media.length === 0)
@@ -261,9 +258,7 @@ export function managedPreparation(target) {
261
258
  }
262
259
  return issues;
263
260
  }
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) {
261
+ export async function uploadManagedMedia(item, presign, config) {
267
262
  if (item.source.kind === "https-url")
268
263
  return httpsUrl(item.source.url).href;
269
264
  if (item.source.kind === "media-ref")
@@ -277,8 +272,7 @@ presign, config) {
277
272
  const source = item.source;
278
273
  const size = item.byteSize ?? (source.kind === "blob" ? source.blob.size : undefined);
279
274
  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 }) }
275
+ ? { filename, contentType: mimeType, ...definedFields({ size }) }
282
276
  : {}));
283
277
  const uploadUrl = string(data[config.provider === "zernio" ? "uploadUrl" : "upload_url"]);
284
278
  const publicUrl = string(data[config.provider === "zernio" ? "publicUrl" : "media_url"]);
@@ -290,19 +284,13 @@ presign, config) {
290
284
  url: uploadUrl,
291
285
  source: {
292
286
  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 } : {}),
287
+ ...definedFields({ size, body: source.kind === "blob" ? source.blob : undefined }),
297
288
  open: source.kind === "blob" ? () => source.blob.stream() : source.open,
298
289
  },
299
290
  allowHost,
300
291
  maxBytes: 5 * 1024 * 1024 * 1024,
301
292
  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 } : {}),
293
+ ...definedFields({ fetch: config.options.fetch, signal: config.context.signal }),
306
294
  });
307
295
  return httpsUrl(publicUrl).href;
308
296
  }
@@ -314,20 +302,12 @@ export function accountMatches(ref, context) {
314
302
  message: "Account reference belongs to another backend instance.",
315
303
  });
316
304
  }
317
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- provider payload is validated at this adapter boundary.
318
305
  export function publicFields(value, fields) {
319
306
  const data = object(value);
320
307
  const result = {};
321
308
  for (const field of fields) {
322
309
  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)
310
+ if (value !== undefined && !isJsonObject(value) && !isJsonArray(value))
331
311
  result[field] = value;
332
312
  }
333
313
  return result;