@opencoredev/social-sdk 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-request.d.ts +15 -0
- package/dist/cli-request.js +193 -0
- package/dist/cli.d.ts +4 -3
- package/dist/cli.js +19 -21
- package/dist/cloud/common.d.ts +7 -6
- package/dist/cloud/common.js +35 -54
- package/dist/cloud/lifecycle.js +31 -35
- package/dist/cloud/media.d.ts +2 -2
- package/dist/cloud/media.js +13 -3
- package/dist/cloud/outcomes.d.ts +4 -3
- package/dist/cloud/outcomes.js +8 -15
- package/dist/cloud/post-for-me.js +41 -49
- package/dist/cloud/zernio.js +58 -98
- package/dist/core/client.js +79 -99
- package/dist/core/fields.d.ts +14 -0
- package/dist/core/fields.js +14 -0
- package/dist/core/idempotency.d.ts +7 -2
- package/dist/core/idempotency.js +37 -20
- package/dist/core/pagination.js +8 -7
- package/dist/core/types.d.ts +3 -2
- package/dist/platforms/bluesky.d.ts +65 -1
- package/dist/platforms/bluesky.js +675 -276
- package/dist/platforms/instagram.d.ts +2 -0
- package/dist/platforms/instagram.js +130 -105
- package/dist/platforms/linkedin.d.ts +58 -1
- package/dist/platforms/linkedin.js +877 -107
- package/dist/platforms/threads.d.ts +13 -1
- package/dist/platforms/threads.js +204 -302
- package/dist/platforms/tiktok.d.ts +4 -0
- package/dist/platforms/tiktok.js +140 -124
- package/dist/platforms/webhook-adapter.d.ts +9 -0
- package/dist/platforms/webhook-adapter.js +24 -0
- package/dist/platforms/x-engagement.js +7 -12
- package/dist/platforms/x-stream.d.ts +83 -0
- package/dist/platforms/x-stream.js +350 -0
- package/dist/platforms/x.d.ts +72 -0
- package/dist/platforms/x.js +328 -119
- package/dist/platforms/youtube-upload.d.ts +1 -1
- package/dist/platforms/youtube-upload.js +6 -2
- package/dist/platforms/youtube.d.ts +28 -4
- package/dist/platforms/youtube.js +291 -133
- package/dist/server/bluesky-oauth.d.ts +177 -0
- package/dist/server/bluesky-oauth.js +1229 -0
- package/dist/server/connections.d.ts +14 -0
- package/dist/server/connections.js +10 -2
- package/dist/server/egress.d.ts +14 -0
- package/dist/server/egress.js +115 -0
- package/dist/server/oauth-internal.d.ts +6 -0
- package/dist/server/oauth-internal.js +66 -0
- package/dist/server/oauth.d.ts +1 -1
- package/dist/server/oauth.js +46 -99
- package/dist/server/webhooks.d.ts +136 -3
- package/dist/server/webhooks.js +639 -25
- package/dist/testing/index.js +14 -28
- package/dist/transport/http.d.ts +1 -1
- package/dist/transport/http.js +0 -1
- package/dist/transport/json.d.ts +7 -0
- package/dist/transport/json.js +32 -4
- package/dist/transport/upload.d.ts +1 -1
- package/dist/transport/upload.js +46 -38
- package/dist/transport/validation.d.ts +16 -5
- package/dist/transport/validation.js +29 -7
- 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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
127
|
-
return finish(2, { error: "Unknown adapter.", adapters:
|
|
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:
|
|
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
|
|
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
|
});
|
package/dist/cloud/common.d.ts
CHANGED
|
@@ -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:
|
|
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<
|
|
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):
|
|
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<
|
|
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:
|
|
26
|
+
export declare function publicFields(value: JsonField, fields: readonly string[]): JsonObject;
|
package/dist/cloud/common.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
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;
|