@opencoredev/social-sdk 0.1.2 → 0.2.1

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 (82) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.d.ts +13 -0
  3. package/dist/cli.js +234 -0
  4. package/dist/cloud/common.d.ts +25 -0
  5. package/dist/cloud/common.js +334 -0
  6. package/dist/cloud/lifecycle.d.ts +10 -0
  7. package/dist/cloud/lifecycle.js +112 -0
  8. package/dist/cloud/media.d.ts +23 -0
  9. package/dist/cloud/media.js +100 -0
  10. package/dist/cloud/outcomes.d.ts +9 -0
  11. package/dist/cloud/outcomes.js +195 -0
  12. package/dist/cloud/post-for-me.d.ts +69 -0
  13. package/dist/cloud/post-for-me.js +396 -0
  14. package/dist/cloud/zernio.d.ts +111 -0
  15. package/dist/cloud/zernio.js +632 -0
  16. package/dist/core/adapter.d.ts +158 -0
  17. package/dist/core/adapter.js +3 -0
  18. package/dist/core/client.d.ts +141 -0
  19. package/dist/core/client.js +1285 -0
  20. package/dist/core/concurrency.d.ts +9 -0
  21. package/dist/core/concurrency.js +79 -0
  22. package/dist/core/errors.d.ts +44 -0
  23. package/dist/core/errors.js +50 -0
  24. package/dist/core/idempotency.d.ts +33 -0
  25. package/dist/core/idempotency.js +58 -0
  26. package/dist/core/index.d.ts +6 -0
  27. package/dist/core/index.js +6 -0
  28. package/dist/core/pagination.d.ts +11 -0
  29. package/dist/core/pagination.js +81 -0
  30. package/dist/core/types.d.ts +396 -0
  31. package/dist/core/types.js +16 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/platforms/bluesky.d.ts +261 -0
  35. package/dist/platforms/bluesky.js +1776 -0
  36. package/dist/platforms/instagram.d.ts +129 -0
  37. package/dist/platforms/instagram.js +1031 -0
  38. package/dist/platforms/linkedin.d.ts +108 -0
  39. package/dist/platforms/linkedin.js +890 -0
  40. package/dist/platforms/threads.d.ts +134 -0
  41. package/dist/platforms/threads.js +945 -0
  42. package/dist/platforms/tiktok.d.ts +31 -0
  43. package/dist/platforms/tiktok.js +593 -0
  44. package/dist/platforms/x-engagement.d.ts +16 -0
  45. package/dist/platforms/x-engagement.js +50 -0
  46. package/dist/platforms/x-text.d.ts +2 -0
  47. package/dist/platforms/x-text.js +123 -0
  48. package/dist/platforms/x-tlds.d.ts +1 -0
  49. package/dist/platforms/x-tlds.js +2 -0
  50. package/dist/platforms/x.d.ts +299 -0
  51. package/dist/platforms/x.js +1287 -0
  52. package/dist/platforms/youtube-upload.d.ts +32 -0
  53. package/dist/platforms/youtube-upload.js +296 -0
  54. package/dist/platforms/youtube.d.ts +108 -0
  55. package/dist/platforms/youtube.js +1100 -0
  56. package/dist/server/connections.d.ts +123 -0
  57. package/dist/server/connections.js +335 -0
  58. package/dist/server/credentials.d.ts +51 -0
  59. package/dist/server/credentials.js +108 -0
  60. package/dist/server/index.d.ts +4 -0
  61. package/dist/server/index.js +4 -0
  62. package/dist/server/oauth.d.ts +52 -0
  63. package/dist/server/oauth.js +553 -0
  64. package/dist/server/webhooks.d.ts +57 -0
  65. package/dist/server/webhooks.js +204 -0
  66. package/dist/testing/index.d.ts +46 -0
  67. package/dist/testing/index.js +564 -0
  68. package/dist/testing.d.ts +1 -0
  69. package/dist/testing.js +1 -0
  70. package/dist/transport/binary.d.ts +2 -0
  71. package/dist/transport/binary.js +29 -0
  72. package/dist/transport/budget.d.ts +3 -0
  73. package/dist/transport/budget.js +26 -0
  74. package/dist/transport/http.d.ts +44 -0
  75. package/dist/transport/http.js +259 -0
  76. package/dist/transport/json.d.ts +8 -0
  77. package/dist/transport/json.js +16 -0
  78. package/dist/transport/upload.d.ts +25 -0
  79. package/dist/transport/upload.js +153 -0
  80. package/dist/transport/validation.d.ts +5 -0
  81. package/dist/transport/validation.js +24 -0
  82. package/package.json +2 -7
@@ -0,0 +1,32 @@
1
+ import type { JsonObject, MediaAttachment } from "../core/types.js";
2
+ export interface YouTubeUploadSession {
3
+ /** Secret upload URI. Store server-side; never serialize into public references. */
4
+ readonly url: string;
5
+ readonly size: number;
6
+ readonly mimeType: string;
7
+ readonly channelId: string;
8
+ }
9
+ export interface YouTubeUploadOptions {
10
+ accessToken: string;
11
+ fetch?: typeof globalThis.fetch;
12
+ signal?: AbortSignal;
13
+ timeoutMs?: number;
14
+ /** Internal absolute deadline shared by a resumable invocation. */
15
+ readonly deadlineAt?: number;
16
+ }
17
+ export declare function beginYouTubeUpload(input: {
18
+ channelId: string;
19
+ size: number;
20
+ mimeType: string;
21
+ metadata: JsonObject;
22
+ }, options: YouTubeUploadOptions): Promise<YouTubeUploadSession>;
23
+ export type YouTubeUploadStatus = {
24
+ state: "incomplete";
25
+ nextByte: number;
26
+ } | {
27
+ state: "complete";
28
+ video: Record<string, unknown>;
29
+ };
30
+ export declare function queryYouTubeUpload(session: YouTubeUploadSession, options: YouTubeUploadOptions): Promise<YouTubeUploadStatus>;
31
+ /** Explicit resumption reopens the caller's replayable source and skips confirmed bytes. */
32
+ export declare function sendYouTubeUpload(session: YouTubeUploadSession, media: MediaAttachment, options: YouTubeUploadOptions, startByte?: number): Promise<YouTubeUploadStatus>;
@@ -0,0 +1,296 @@
1
+ import { SocialError } from "../core/errors.js";
2
+ import { readJson } from "../transport/http.js";
3
+ import { array, object, optionalString, string } from "../transport/validation.js";
4
+ const deadlines = new WeakMap();
5
+ function deadlineFor(options) {
6
+ if (options.deadlineAt !== undefined)
7
+ return options.deadlineAt;
8
+ const existing = deadlines.get(options);
9
+ if (existing !== undefined)
10
+ return existing;
11
+ const deadline = Date.now() + (options.timeoutMs ?? 120_000);
12
+ deadlines.set(options, deadline);
13
+ return deadline;
14
+ }
15
+ function remaining(options) {
16
+ const limit = deadlineFor(options);
17
+ return limit - Date.now();
18
+ }
19
+ async function bounded(operation, timeoutMs, message, signal) {
20
+ if (timeoutMs <= 0) {
21
+ operation.catch(() => undefined);
22
+ throw new SocialError({
23
+ code: "timeout",
24
+ operation: "youtube.upload",
25
+ message,
26
+ retryDisposition: { kind: "reconcile-first" },
27
+ });
28
+ }
29
+ let timer;
30
+ const timeout = new Promise((_, reject) => {
31
+ timer = setTimeout(() => reject(new SocialError({
32
+ code: "timeout",
33
+ operation: "youtube.upload",
34
+ message,
35
+ retryDisposition: { kind: "reconcile-first" },
36
+ })), timeoutMs);
37
+ });
38
+ operation.catch(() => undefined);
39
+ const cancelled = signal
40
+ ? new Promise((_, reject) => signal.addEventListener("abort", () => reject(new SocialError({
41
+ code: "cancelled",
42
+ operation: "youtube.upload",
43
+ message: "Upload was cancelled.",
44
+ retryDisposition: { kind: "never" },
45
+ })), { once: true }))
46
+ : undefined;
47
+ try {
48
+ return await Promise.race(cancelled ? [operation, timeout, cancelled] : [operation, timeout]);
49
+ }
50
+ finally {
51
+ if (timer)
52
+ clearTimeout(timer);
53
+ }
54
+ }
55
+ function sessionUrl(value) {
56
+ const url = new URL(value);
57
+ if (url.origin !== "https://www.googleapis.com" ||
58
+ url.pathname !== "/upload/youtube/v3/videos" ||
59
+ url.username ||
60
+ url.password) {
61
+ throw new SocialError({
62
+ code: "media_error",
63
+ operation: "youtube.upload",
64
+ message: "YouTube returned an unexpected upload origin or route.",
65
+ });
66
+ }
67
+ return url;
68
+ }
69
+ async function uploadRequest(url, init, options) {
70
+ if (options.signal?.aborted)
71
+ throw new SocialError({
72
+ code: "cancelled",
73
+ operation: "youtube.upload",
74
+ message: "Upload was cancelled before this request.",
75
+ });
76
+ if (remaining(options) <= 0)
77
+ throw new SocialError({
78
+ code: "timeout",
79
+ operation: "youtube.upload",
80
+ message: "YouTube upload deadline expired before dispatch.",
81
+ retryDisposition: { kind: "reconcile-first" },
82
+ });
83
+ const controller = new AbortController();
84
+ const abort = () => controller.abort(options.signal?.reason);
85
+ options.signal?.addEventListener("abort", abort, { once: true });
86
+ const timer = setTimeout(() => controller.abort(), remaining(options));
87
+ try {
88
+ const response = await bounded((options.fetch ?? globalThis.fetch)(url, {
89
+ ...init,
90
+ headers: { Authorization: `Bearer ${options.accessToken}`, ...init.headers },
91
+ signal: controller.signal,
92
+ redirect: "error",
93
+ }), remaining(options), "YouTube upload request exceeded its total deadline.", options.signal);
94
+ // Read response within the deadline, then return a bounded in-memory response.
95
+ if (response.status === 308) {
96
+ void response.body?.cancel().catch(() => undefined);
97
+ return new Response(null, { status: response.status, headers: response.headers });
98
+ }
99
+ if (!response.ok) {
100
+ let reason;
101
+ try {
102
+ const payload = object(await readJson(response.clone(), 64 * 1024));
103
+ // Google nests reasons under error.errors; accept a top-level list too.
104
+ const errors = array(payload["error"] === undefined ? payload["errors"] : object(payload["error"])["errors"]);
105
+ const first = errors[0];
106
+ if (first !== undefined)
107
+ reason = optionalString(object(first)["reason"]);
108
+ }
109
+ catch {
110
+ // Preserve the HTTP status when the provider omits a parseable error body.
111
+ }
112
+ void response.body?.cancel().catch(() => undefined);
113
+ throw new SocialError({
114
+ code: response.status === 401
115
+ ? "reconnect_required"
116
+ : reason === "quotaExceeded"
117
+ ? "rate_limited"
118
+ : response.status >= 500
119
+ ? "ambiguous_outcome"
120
+ : "media_error",
121
+ operation: "youtube.upload",
122
+ message: `YouTube upload request failed with HTTP ${response.status}.`,
123
+ upstreamStatus: response.status,
124
+ retryDisposition: response.status >= 500
125
+ ? { kind: "reconcile-first" }
126
+ : response.status === 401
127
+ ? { kind: "after-reconnect" }
128
+ : { kind: "never" },
129
+ });
130
+ }
131
+ if (response.status === 204 || response.headers.get("content-length") === "0") {
132
+ void response.body?.cancel().catch(() => undefined);
133
+ return new Response(null, { status: response.status, headers: response.headers });
134
+ }
135
+ if (init.method === "POST") {
136
+ void response.body?.cancel().catch(() => undefined);
137
+ return new Response(null, { status: response.status, headers: response.headers });
138
+ }
139
+ const body = await bounded(readJson(response, 2 * 1024 * 1024, controller.signal), remaining(options), "YouTube upload response exceeded its total deadline.", options.signal);
140
+ return Response.json(body, { status: response.status, headers: response.headers });
141
+ }
142
+ catch (error) {
143
+ if (error instanceof SocialError)
144
+ throw error;
145
+ throw new SocialError({
146
+ code: "ambiguous_outcome",
147
+ operation: "youtube.upload",
148
+ message: "YouTube upload acceptance is uncertain. Query the saved session before resuming.",
149
+ retryDisposition: { kind: "reconcile-first" },
150
+ });
151
+ }
152
+ finally {
153
+ clearTimeout(timer);
154
+ options.signal?.removeEventListener("abort", abort);
155
+ }
156
+ }
157
+ export async function beginYouTubeUpload(input, options) {
158
+ if (!Number.isSafeInteger(input.size) ||
159
+ input.size <= 0 ||
160
+ input.size > 274877906944 ||
161
+ !input.mimeType.startsWith("video/"))
162
+ throw new SocialError({
163
+ code: "invalid_input",
164
+ operation: "youtube.upload",
165
+ message: "Provide a positive video byte size no larger than 256 GiB and its video MIME type.",
166
+ });
167
+ const response = await uploadRequest(new URL("https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status,processingDetails"), {
168
+ method: "POST",
169
+ headers: {
170
+ "Content-Type": "application/json; charset=UTF-8",
171
+ "X-Upload-Content-Length": String(input.size),
172
+ "X-Upload-Content-Type": input.mimeType,
173
+ },
174
+ body: JSON.stringify(input.metadata),
175
+ }, options);
176
+ const url = string(response.headers.get("location"));
177
+ sessionUrl(url);
178
+ return { url, size: input.size, mimeType: input.mimeType, channelId: input.channelId };
179
+ }
180
+ async function statusFrom(response) {
181
+ if (response.status !== 308)
182
+ return { state: "complete", video: object(await response.json()) };
183
+ const range = response.headers.get("range");
184
+ if (!range)
185
+ return { state: "incomplete", nextByte: 0 };
186
+ const matched = /^bytes=0-(\d+)$/.exec(range);
187
+ if (!matched?.[1])
188
+ throw new SocialError({
189
+ code: "media_error",
190
+ operation: "youtube.upload",
191
+ message: "YouTube returned an invalid upload range.",
192
+ });
193
+ const nextByte = Number(matched[1]) + 1;
194
+ if (!Number.isSafeInteger(nextByte))
195
+ throw new SocialError({
196
+ code: "media_error",
197
+ operation: "youtube.upload",
198
+ message: "YouTube returned an invalid upload offset.",
199
+ });
200
+ return { state: "incomplete", nextByte };
201
+ }
202
+ export async function queryYouTubeUpload(session, options) {
203
+ return statusFrom(await uploadRequest(sessionUrl(session.url), {
204
+ method: "PUT",
205
+ headers: { "Content-Range": `bytes */${session.size}`, "Content-Length": "0" },
206
+ }, options));
207
+ }
208
+ /** Explicit resumption reopens the caller's replayable source and skips confirmed bytes. */
209
+ export async function sendYouTubeUpload(session, media, options, startByte = 0) {
210
+ if (!Number.isSafeInteger(startByte) || startByte < 0 || startByte >= session.size)
211
+ throw new SocialError({
212
+ code: "invalid_input",
213
+ operation: "youtube.upload",
214
+ message: "Resume at a confirmed offset within the file.",
215
+ });
216
+ const operationOptions = options;
217
+ const source = media.source;
218
+ if (source.kind !== "blob" && source.kind !== "stream")
219
+ throw new SocialError({
220
+ code: "invalid_input",
221
+ operation: "youtube.upload",
222
+ message: "Direct YouTube uploads require a Blob or replayable stream; remote URLs are not fetched.",
223
+ });
224
+ const reader = (source.kind === "blob" ? source.blob.stream() : source.open()).getReader();
225
+ const chunkBytes = 8 * 1024 * 1024;
226
+ let buffered = new Uint8Array(0);
227
+ let cursor = 0;
228
+ let offset = startByte;
229
+ let skipped = 0;
230
+ let ended = false;
231
+ try {
232
+ while (offset < session.size) {
233
+ const wanted = Math.min(chunkBytes, session.size - offset);
234
+ const chunk = new Uint8Array(wanted);
235
+ let written = 0;
236
+ while (written < wanted) {
237
+ operationOptions.signal?.throwIfAborted();
238
+ if (cursor === buffered.byteLength) {
239
+ const next = await bounded(reader.read(), remaining(operationOptions), "YouTube upload source exceeded its total deadline.", operationOptions.signal);
240
+ if (next.done) {
241
+ ended = true;
242
+ break;
243
+ }
244
+ buffered = new Uint8Array(next.value);
245
+ cursor = 0;
246
+ }
247
+ if (skipped < startByte) {
248
+ const skip = Math.min(startByte - skipped, buffered.byteLength - cursor);
249
+ skipped += skip;
250
+ cursor += skip;
251
+ continue;
252
+ }
253
+ const take = Math.min(wanted - written, buffered.byteLength - cursor);
254
+ chunk.set(buffered.subarray(cursor, cursor + take), written);
255
+ written += take;
256
+ cursor += take;
257
+ }
258
+ if (ended || written !== wanted)
259
+ throw new SocialError({
260
+ code: "media_error",
261
+ operation: "youtube.upload",
262
+ message: "Source ended before its declared byte size.",
263
+ });
264
+ if (offset + wanted === session.size) {
265
+ // Confirm the declared size before sending the chunk that finalizes publication.
266
+ if (cursor < buffered.byteLength ||
267
+ !(await bounded(reader.read(), remaining(operationOptions), "YouTube upload source exceeded its total deadline.", operationOptions.signal)).done) {
268
+ throw new SocialError({
269
+ code: "media_error",
270
+ operation: "youtube.upload",
271
+ message: "Source exceeds its declared byte size.",
272
+ });
273
+ }
274
+ }
275
+ const result = await statusFrom(await uploadRequest(sessionUrl(session.url), {
276
+ method: "PUT",
277
+ headers: {
278
+ "Content-Type": session.mimeType,
279
+ "Content-Length": String(wanted),
280
+ "Content-Range": `bytes ${offset}-${offset + wanted - 1}/${session.size}`,
281
+ },
282
+ body: chunk,
283
+ }, operationOptions));
284
+ if (result.state === "complete")
285
+ return result;
286
+ if (result.nextByte !== offset + wanted)
287
+ return result;
288
+ offset = result.nextByte;
289
+ }
290
+ return { state: "incomplete", nextByte: offset };
291
+ }
292
+ finally {
293
+ void reader.cancel().catch(() => undefined);
294
+ reader.releaseLock();
295
+ }
296
+ }
@@ -0,0 +1,108 @@
1
+ import type { AdapterOperationContext, JsonObject, MediaAttachment } from "../core/types.js";
2
+ import { type YouTubeUploadSession } from "./youtube-upload.js";
3
+ export interface YouTubeOptions {
4
+ readonly auth: {
5
+ readonly accessToken: string;
6
+ readonly channelId: string;
7
+ };
8
+ readonly fetch?: typeof globalThis.fetch;
9
+ readonly clock?: () => Date;
10
+ /** Persist secret resumable URI server-side before uploading any video bytes. */
11
+ readonly saveUploadSession?: (session: YouTubeUploadSession) => Promise<void>;
12
+ }
13
+ export interface YouTubeNative {
14
+ readonly resumeUpload: (session: YouTubeUploadSession, media: MediaAttachment, context: AdapterOperationContext) => Promise<unknown>;
15
+ readonly queryUpload: (session: YouTubeUploadSession, context: AdapterOperationContext) => Promise<unknown>;
16
+ readonly setThumbnail: (input: {
17
+ readonly videoId: string;
18
+ readonly thumbnail: MediaAttachment;
19
+ readonly context: AdapterOperationContext;
20
+ }) => Promise<JsonObject>;
21
+ readonly captions: (input: {
22
+ readonly action: "list" | "insert" | "update" | "delete" | "download";
23
+ readonly videoId: string;
24
+ readonly caption?: MediaAttachment;
25
+ readonly captionId?: string;
26
+ readonly body?: JsonObject;
27
+ readonly context: AdapterOperationContext;
28
+ }) => Promise<JsonObject | Blob>;
29
+ readonly playlists: (input: {
30
+ readonly action: "list" | "insert" | "update" | "delete";
31
+ readonly playlistId?: string;
32
+ readonly channelId?: string;
33
+ readonly mine?: boolean;
34
+ readonly pageToken?: string;
35
+ readonly maxResults?: number;
36
+ readonly body?: JsonObject;
37
+ readonly context: AdapterOperationContext;
38
+ }) => Promise<JsonObject>;
39
+ readonly playlistItems: (input: {
40
+ readonly action: "list" | "insert" | "update" | "delete";
41
+ readonly playlistId?: string;
42
+ readonly playlistItemId?: string;
43
+ readonly body?: JsonObject;
44
+ readonly pageToken?: string;
45
+ readonly context: AdapterOperationContext;
46
+ }) => Promise<JsonObject>;
47
+ readonly rateVideo: (input: {
48
+ readonly videoId: string;
49
+ readonly rating: "like" | "dislike" | "none";
50
+ readonly context: AdapterOperationContext;
51
+ }) => Promise<void>;
52
+ readonly getRating: (input: {
53
+ readonly videoIds: readonly string[];
54
+ readonly context: AdapterOperationContext;
55
+ }) => Promise<JsonObject>;
56
+ readonly subscriptions: (input: {
57
+ readonly action: "list" | "insert" | "delete";
58
+ readonly subscriptionId?: string;
59
+ readonly channelId?: string;
60
+ readonly pageToken?: string;
61
+ readonly context: AdapterOperationContext;
62
+ }) => Promise<JsonObject>;
63
+ readonly search: (input: {
64
+ readonly q: string;
65
+ readonly type?: string;
66
+ readonly channelId?: string;
67
+ readonly order?: string;
68
+ readonly publishedAfter?: string;
69
+ readonly publishedBefore?: string;
70
+ readonly pageToken?: string;
71
+ readonly maxResults?: number;
72
+ readonly context: AdapterOperationContext;
73
+ }) => Promise<JsonObject>;
74
+ readonly commentsModeration: (input: {
75
+ readonly action: "setModerationStatus" | "delete" | "update";
76
+ readonly commentId: string;
77
+ readonly moderationStatus?: "published" | "heldForReview" | "rejected";
78
+ readonly banAuthor?: boolean;
79
+ readonly body?: JsonObject;
80
+ readonly context: AdapterOperationContext;
81
+ }) => Promise<JsonObject | void>;
82
+ readonly heldComments: (input: {
83
+ readonly pageToken?: string;
84
+ readonly maxResults?: number;
85
+ readonly context: AdapterOperationContext;
86
+ }) => Promise<JsonObject>;
87
+ readonly updateVideo: (input: {
88
+ readonly videoId: string;
89
+ readonly body: JsonObject;
90
+ readonly context: AdapterOperationContext;
91
+ }) => Promise<JsonObject>;
92
+ readonly deleteVideo: (input: {
93
+ readonly videoId: string;
94
+ readonly context: AdapterOperationContext;
95
+ }) => Promise<void>;
96
+ readonly analytics: (input: {
97
+ readonly query: Record<string, string>;
98
+ readonly context: AdapterOperationContext;
99
+ }) => Promise<JsonObject>;
100
+ readonly liveBroadcasts: (input: {
101
+ readonly action: "list" | "insert" | "transition";
102
+ readonly body?: JsonObject;
103
+ readonly id?: string;
104
+ readonly broadcastStatus?: "testing" | "live" | "complete";
105
+ readonly context: AdapterOperationContext;
106
+ }) => Promise<JsonObject>;
107
+ }
108
+ export declare function youtube(options: YouTubeOptions): import("../core/adapter.js").SocialAdapter<YouTubeNative>;