@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.
- 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 +24 -21
- package/dist/cloud/common.d.ts +13 -8
- package/dist/cloud/common.js +40 -60
- package/dist/cloud/lifecycle.js +31 -35
- package/dist/cloud/media.d.ts +6 -2
- package/dist/cloud/media.js +50 -37
- 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/postfast.d.ts +54 -0
- package/dist/cloud/postfast.js +578 -0
- 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 +6 -2
package/dist/cloud/zernio.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/* oxlint-disable anti-slop/no-runtime-typeof -- validated external boundary or fixture contract. */
|
|
2
1
|
import { managedLifecycle } from "./lifecycle.js";
|
|
3
2
|
import { managedMedia } from "./media.js";
|
|
4
3
|
export { MemoryManagedMediaStore, } from "./media.js";
|
|
5
4
|
import { defineAdapter } from "../core/adapter.js";
|
|
6
5
|
import { SocialError } from "../core/errors.js";
|
|
7
|
-
import {
|
|
6
|
+
import { definedFields } from "../core/fields.js";
|
|
7
|
+
import { array, isBoolean, isFiniteNumber, isJsonArray, isJsonObject, isString, object, optionalNumber, optionalString, string, } from "../transport/validation.js";
|
|
8
8
|
import { decodeWebhook, verifyZernioWebhook } from "../server/webhooks.js";
|
|
9
9
|
import { accountMatches, capabilityManifest, managedHttp, managedPreparation, managedOptionIssues, optionsObject, platform, publicFields, } from "./common.js";
|
|
10
10
|
import { zernioOutcome } from "./outcomes.js";
|
|
@@ -12,7 +12,6 @@ export function zernio(options) {
|
|
|
12
12
|
const request = managedHttp("https://zernio.com/api", options);
|
|
13
13
|
const mediaPipeline = managedMedia("zernio", options, (body, context) => request("/v1/media/presign", context, body));
|
|
14
14
|
const now = () => (options.clock?.() ?? new Date()).toISOString();
|
|
15
|
-
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- provider payload is validated at this adapter boundary.
|
|
16
15
|
const account = (value, backend) => {
|
|
17
16
|
const row = object(value);
|
|
18
17
|
const handle = optionalString(row["username"]);
|
|
@@ -25,8 +24,7 @@ export function zernio(options) {
|
|
|
25
24
|
accountId: string(row["_id"]),
|
|
26
25
|
},
|
|
27
26
|
displayName: optionalString(row["displayName"]) ?? handle ?? string(row["_id"]),
|
|
28
|
-
|
|
29
|
-
...(handle ? { handle } : {}),
|
|
27
|
+
...definedFields({ handle: handle || undefined }),
|
|
30
28
|
status: row["isActive"] === true
|
|
31
29
|
? "connected"
|
|
32
30
|
: row["isActive"] === false
|
|
@@ -34,7 +32,6 @@ export function zernio(options) {
|
|
|
34
32
|
: "unknown",
|
|
35
33
|
};
|
|
36
34
|
};
|
|
37
|
-
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- validated by account at this boundary.
|
|
38
35
|
const supportedAccount = (value, backend) => {
|
|
39
36
|
try {
|
|
40
37
|
return account(value, backend);
|
|
@@ -127,8 +124,9 @@ export function zernio(options) {
|
|
|
127
124
|
const parsed = supportedAccount(value, context.backendInstance);
|
|
128
125
|
return parsed ? [parsed] : [];
|
|
129
126
|
}),
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
...definedFields({
|
|
128
|
+
nextCursor: pages !== undefined && page < pages ? String(page + 1) : undefined,
|
|
129
|
+
}),
|
|
132
130
|
};
|
|
133
131
|
},
|
|
134
132
|
async get(ref, context) {
|
|
@@ -193,55 +191,33 @@ export function zernio(options) {
|
|
|
193
191
|
const rows = array(result["posts"])
|
|
194
192
|
.map(object)
|
|
195
193
|
.flatMap((row) => {
|
|
196
|
-
const destinations =
|
|
194
|
+
const destinations = isJsonArray(row["platforms"])
|
|
197
195
|
? array(row["platforms"]).map(object)
|
|
198
196
|
: [];
|
|
199
197
|
const destination = destinations.find((item) => {
|
|
200
198
|
const value = item["accountId"];
|
|
201
|
-
const id =
|
|
202
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
|
|
203
|
-
typeof value === "string"
|
|
199
|
+
const id = isString(value)
|
|
204
200
|
? value
|
|
205
|
-
:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
: undefined;
|
|
201
|
+
: isJsonObject(value) || isJsonArray(value)
|
|
202
|
+
? object(value)["_id"]
|
|
203
|
+
: undefined;
|
|
209
204
|
return (id === account.accountId &&
|
|
210
205
|
(item["platform"] === account.platform ||
|
|
211
206
|
item["platform"] === (account.platform === "x" ? "twitter" : account.platform)));
|
|
212
207
|
});
|
|
213
208
|
if (destination === undefined)
|
|
214
209
|
return [];
|
|
215
|
-
const platformPostId =
|
|
216
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
|
|
217
|
-
typeof destination["platformPostId"] === "string"
|
|
218
|
-
? destination["platformPostId"]
|
|
219
|
-
: undefined;
|
|
220
210
|
return [
|
|
221
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
|
|
222
211
|
{
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
228
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
229
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
230
|
-
...(typeof row["_id"] === "string" ? { backendPostId: row["_id"] } : {}),
|
|
231
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
|
|
232
|
-
...(platformPostId === undefined ? {} : { platformPostId }),
|
|
212
|
+
...definedFields({
|
|
213
|
+
backendPostId: optionalString(row["_id"]),
|
|
214
|
+
platformPostId: optionalString(destination["platformPostId"]),
|
|
215
|
+
}),
|
|
233
216
|
platform: account.platform,
|
|
234
217
|
accountId: account.accountId,
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
239
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
240
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
241
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
242
|
-
...(typeof destination["platformPostUrl"] === "string"
|
|
243
|
-
? { platformPostUrl: destination["platformPostUrl"] }
|
|
244
|
-
: {}),
|
|
218
|
+
...definedFields({
|
|
219
|
+
platformPostUrl: optionalString(destination["platformPostUrl"]),
|
|
220
|
+
}),
|
|
245
221
|
...publicFields(row, [
|
|
246
222
|
"content",
|
|
247
223
|
"caption",
|
|
@@ -257,12 +233,10 @@ export function zernio(options) {
|
|
|
257
233
|
const pagination = result["pagination"] === undefined ? {} : object(result["pagination"]);
|
|
258
234
|
const totalPages = optionalNumber(pagination["pages"]) ?? optionalNumber(pagination["totalPages"]);
|
|
259
235
|
return {
|
|
260
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- provider payload is validated at this adapter boundary.
|
|
261
236
|
items: rows,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
: {}),
|
|
237
|
+
...definedFields({
|
|
238
|
+
nextCursor: totalPages !== undefined && page < totalPages ? String(page + 1) : undefined,
|
|
239
|
+
}),
|
|
266
240
|
};
|
|
267
241
|
},
|
|
268
242
|
prepareTarget(target) {
|
|
@@ -291,23 +265,20 @@ export function zernio(options) {
|
|
|
291
265
|
media.push({
|
|
292
266
|
type: item.kind,
|
|
293
267
|
url: await mediaPipeline.resolve(item, target.account, context),
|
|
294
|
-
|
|
295
|
-
...(item.altText === undefined ? {} : { altText: item.altText }),
|
|
296
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
|
|
297
|
-
...(item.mimeType === undefined ? {} : { mimeType: item.mimeType }),
|
|
268
|
+
...definedFields({ altText: item.altText, mimeType: item.mimeType }),
|
|
298
269
|
});
|
|
299
270
|
const config = optionsObject(target);
|
|
300
271
|
const native = {};
|
|
301
272
|
if (target.account.platform === "youtube") {
|
|
302
273
|
native["title"] = string(config["title"]);
|
|
303
274
|
native["visibility"] = string(config["visibility"]);
|
|
304
|
-
|
|
305
|
-
if (
|
|
306
|
-
native["madeForKids"] =
|
|
275
|
+
const madeForKids = config["madeForKids"];
|
|
276
|
+
if (isBoolean(madeForKids))
|
|
277
|
+
native["madeForKids"] = madeForKids;
|
|
307
278
|
}
|
|
308
|
-
|
|
309
|
-
if (target.account.platform === "instagram" &&
|
|
310
|
-
native["shareToFeed"] =
|
|
279
|
+
const shareToFeed = config["shareToFeed"];
|
|
280
|
+
if (target.account.platform === "instagram" && isBoolean(shareToFeed))
|
|
281
|
+
native["shareToFeed"] = shareToFeed;
|
|
311
282
|
if (target.account.platform === "x" &&
|
|
312
283
|
config["replySettings"] !== undefined &&
|
|
313
284
|
config["replySettings"] !== "everyone")
|
|
@@ -319,21 +290,24 @@ export function zernio(options) {
|
|
|
319
290
|
native["autoAddMusic"] = false;
|
|
320
291
|
native["allowDuet"] = !config["disableDuet"];
|
|
321
292
|
native["allowStitch"] = !config["disableStitch"];
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
293
|
+
// prepareTarget requires these TikTok choices to be booleans. Skipping an
|
|
294
|
+
// absent value sends the same JSON body, since serialization drops undefined.
|
|
295
|
+
for (const [nativeKey, optionKey] of [
|
|
296
|
+
["isBrandOrganicPost", "ownBrand"],
|
|
297
|
+
["videoMadeWithAi", "aiGenerated"],
|
|
298
|
+
["draft", "draft"],
|
|
299
|
+
]) {
|
|
300
|
+
const value = config[optionKey];
|
|
301
|
+
if (value !== undefined)
|
|
302
|
+
native[nativeKey] = value;
|
|
303
|
+
}
|
|
304
|
+
const { photoCoverIndex, disableComments, brandedContent } = config;
|
|
305
|
+
if (isFiniteNumber(photoCoverIndex))
|
|
306
|
+
native["photoCoverIndex"] = photoCoverIndex;
|
|
307
|
+
if (isBoolean(disableComments))
|
|
308
|
+
native["allowComment"] = !disableComments;
|
|
309
|
+
if (isBoolean(brandedContent))
|
|
310
|
+
native["brandPartnerPromote"] = brandedContent;
|
|
337
311
|
}
|
|
338
312
|
const response = await request("/v1/posts", context, {
|
|
339
313
|
content: target.content.text ?? "",
|
|
@@ -342,15 +316,15 @@ export function zernio(options) {
|
|
|
342
316
|
{
|
|
343
317
|
platform: target.account.platform === "x" ? "twitter" : target.account.platform,
|
|
344
318
|
accountId: target.account.accountId,
|
|
345
|
-
|
|
346
|
-
|
|
319
|
+
...definedFields({
|
|
320
|
+
platformSpecificData: Object.keys(native).length ? native : undefined,
|
|
321
|
+
}),
|
|
347
322
|
},
|
|
348
323
|
],
|
|
349
324
|
...(target.schedule
|
|
350
325
|
? {
|
|
351
326
|
scheduledFor: target.schedule.at,
|
|
352
|
-
|
|
353
|
-
...(target.schedule.timeZone ? { timezone: target.schedule.timeZone } : {}),
|
|
327
|
+
...definedFields({ timezone: target.schedule.timeZone || undefined }),
|
|
354
328
|
}
|
|
355
329
|
: { publishNow: true }),
|
|
356
330
|
});
|
|
@@ -471,8 +445,7 @@ export function zernio(options) {
|
|
|
471
445
|
const result = object(await request(`/v1/inbox/comments/${encodeURIComponent(ref.postId)}`, context, undefined, {
|
|
472
446
|
accountId: ref.accountId,
|
|
473
447
|
limit: String(limit),
|
|
474
|
-
|
|
475
|
-
...(input.cursor === undefined ? {} : { cursor: input.cursor }),
|
|
448
|
+
...definedFields({ cursor: input.cursor }),
|
|
476
449
|
}));
|
|
477
450
|
const pagination = result["pagination"] ? object(result["pagination"]) : {};
|
|
478
451
|
const cursor = optionalString(pagination["cursor"]);
|
|
@@ -492,8 +465,7 @@ export function zernio(options) {
|
|
|
492
465
|
"canDelete",
|
|
493
466
|
"canHide",
|
|
494
467
|
])),
|
|
495
|
-
|
|
496
|
-
...(hasMore === false || !cursor ? {} : { nextCursor: cursor }),
|
|
468
|
+
...definedFields({ nextCursor: hasMore === false || !cursor ? undefined : cursor }),
|
|
497
469
|
};
|
|
498
470
|
},
|
|
499
471
|
async reply(ref, content, context) {
|
|
@@ -525,8 +497,7 @@ export function zernio(options) {
|
|
|
525
497
|
const result = object(await request("/v1/inbox/conversations", context, undefined, {
|
|
526
498
|
accountId: ref.accountId,
|
|
527
499
|
limit: String(limit),
|
|
528
|
-
|
|
529
|
-
...(input.cursor === undefined ? {} : { cursor: input.cursor }),
|
|
500
|
+
...definedFields({ cursor: input.cursor }),
|
|
530
501
|
}));
|
|
531
502
|
const pagination = result["pagination"] === undefined ? {} : object(result["pagination"]);
|
|
532
503
|
const cursor = optionalString(pagination["nextCursor"]);
|
|
@@ -541,8 +512,7 @@ export function zernio(options) {
|
|
|
541
512
|
"updatedTime",
|
|
542
513
|
"unreadCount",
|
|
543
514
|
])),
|
|
544
|
-
|
|
545
|
-
...(cursor ? { nextCursor: cursor } : {}),
|
|
515
|
+
...definedFields({ nextCursor: cursor || undefined }),
|
|
546
516
|
};
|
|
547
517
|
},
|
|
548
518
|
async listMessages(ref, input, context) {
|
|
@@ -557,8 +527,7 @@ export function zernio(options) {
|
|
|
557
527
|
const result = object(await request(`/v1/inbox/conversations/${encodeURIComponent(ref.conversationId)}/messages`, context, undefined, {
|
|
558
528
|
accountId: ref.accountId,
|
|
559
529
|
limit: String(limit),
|
|
560
|
-
|
|
561
|
-
...(input.cursor === undefined ? {} : { cursor: input.cursor }),
|
|
530
|
+
...definedFields({ cursor: input.cursor }),
|
|
562
531
|
}));
|
|
563
532
|
const pagination = result["pagination"] ? object(result["pagination"]) : {};
|
|
564
533
|
const cursor = optionalString(pagination["nextCursor"]);
|
|
@@ -572,8 +541,7 @@ export function zernio(options) {
|
|
|
572
541
|
"createdAt",
|
|
573
542
|
"deliveryStatus",
|
|
574
543
|
])),
|
|
575
|
-
|
|
576
|
-
...(cursor ? { nextCursor: cursor } : {}),
|
|
544
|
+
...definedFields({ nextCursor: cursor || undefined }),
|
|
577
545
|
};
|
|
578
546
|
},
|
|
579
547
|
async send(ref, content, context) {
|
|
@@ -587,8 +555,7 @@ export function zernio(options) {
|
|
|
587
555
|
});
|
|
588
556
|
const data = object(result["data"]);
|
|
589
557
|
const messageId = optionalString(data["messageId"]);
|
|
590
|
-
|
|
591
|
-
return { state: "sent", ...(messageId ? { messageId } : {}) };
|
|
558
|
+
return { state: "sent", ...definedFields({ messageId: messageId || undefined }) };
|
|
592
559
|
},
|
|
593
560
|
},
|
|
594
561
|
webhooks: {
|
|
@@ -613,14 +580,7 @@ export function zernio(options) {
|
|
|
613
580
|
const response = object(await request(`/v1/connect/${input.platform === "x" ? "twitter" : encodeURIComponent(input.platform)}`, context, undefined, { profileId: input.profileId, redirect_url: input.redirectUrl }));
|
|
614
581
|
return {
|
|
615
582
|
url: string(response["authUrl"]),
|
|
616
|
-
|
|
617
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider payload is validated at this adapter boundary.
|
|
618
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
619
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
620
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
621
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
622
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
623
|
-
...(typeof response["state"] === "string" ? { providerState: response["state"] } : {}),
|
|
583
|
+
...definedFields({ providerState: optionalString(response["state"]) }),
|
|
624
584
|
};
|
|
625
585
|
},
|
|
626
586
|
async listProfiles(context) {
|
package/dist/core/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/* oxlint-disable anti-slop/require-readable-spacing, anti-slop/no-conditional-empty-object-spread, anti-slop/require-safety-comment-for-type-assertion -- facade dispatch keeps capability-specific branches together. */
|
|
2
1
|
import { decodeCursor, encodeCursor, iterateItems } from "./pagination.js";
|
|
3
2
|
import { createConcurrencyLimiter } from "./concurrency.js";
|
|
4
3
|
import { SocialError } from "./errors.js";
|
|
4
|
+
import { definedFields } from "./fields.js";
|
|
5
5
|
import { deriveTargetIdempotencyKey, fingerprint, } from "./idempotency.js";
|
|
6
6
|
function targetKey(account) {
|
|
7
7
|
return JSON.stringify([account.backend, account.platform, account.accountId]);
|
|
@@ -96,10 +96,19 @@ function unsupported(operation, backend) {
|
|
|
96
96
|
message: `${backend} does not implement ${operation}`,
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Runtime guards for JavaScript callers that bypass the facade's static types.
|
|
101
|
+
* They are generic so a typed argument keeps its declared type when narrowed.
|
|
102
|
+
*/
|
|
103
|
+
function isObjectInput(value) {
|
|
104
|
+
return typeof value === "object" && value !== null;
|
|
105
|
+
}
|
|
106
|
+
function isNonBlankStringInput(value) {
|
|
107
|
+
return typeof value === "string" && value.trim() !== "";
|
|
108
|
+
}
|
|
109
|
+
/** `error` is the rejection when it was a SocialError, or undefined for any other thrown value. */
|
|
110
|
+
function outcomeFromError(error, target, observedAt) {
|
|
111
|
+
if (error !== undefined) {
|
|
103
112
|
if (error.code === "cancelled") {
|
|
104
113
|
return {
|
|
105
114
|
state: "unknown",
|
|
@@ -186,15 +195,15 @@ export function createSocial(config) {
|
|
|
186
195
|
message: `Unknown backend instance: ${backend}`,
|
|
187
196
|
});
|
|
188
197
|
}
|
|
189
|
-
return limiter(signal, operation, work).catch((
|
|
190
|
-
if (
|
|
191
|
-
throw
|
|
198
|
+
return limiter(signal, operation, work).catch((cause) => {
|
|
199
|
+
if (cause instanceof SocialError)
|
|
200
|
+
throw cause;
|
|
192
201
|
throw new SocialError({
|
|
193
202
|
code: "upstream_failure",
|
|
194
203
|
operation,
|
|
195
204
|
backend,
|
|
196
205
|
message: "The adapter failed while processing the request",
|
|
197
|
-
cause
|
|
206
|
+
cause,
|
|
198
207
|
});
|
|
199
208
|
});
|
|
200
209
|
}
|
|
@@ -311,15 +320,12 @@ export function createSocial(config) {
|
|
|
311
320
|
}
|
|
312
321
|
}
|
|
313
322
|
function validateAccountRef(account, operation) {
|
|
314
|
-
if (account
|
|
315
|
-
typeof account !== "object" ||
|
|
323
|
+
if (!isObjectInput(account) ||
|
|
316
324
|
account.kind !== "connected-account" ||
|
|
317
325
|
account.version !== 1 ||
|
|
318
|
-
|
|
319
|
-
!account.backend.trim() ||
|
|
326
|
+
!isNonBlankStringInput(account.backend) ||
|
|
320
327
|
!account.platform ||
|
|
321
|
-
|
|
322
|
-
!account.accountId.trim())
|
|
328
|
+
!isNonBlankStringInput(account.accountId))
|
|
323
329
|
throw new SocialError({
|
|
324
330
|
code: "invalid_input",
|
|
325
331
|
operation,
|
|
@@ -494,7 +500,7 @@ export function createSocial(config) {
|
|
|
494
500
|
observedAt: observedAt(),
|
|
495
501
|
reason: "capacity",
|
|
496
502
|
}
|
|
497
|
-
: outcomeFromError(error, target, observedAt());
|
|
503
|
+
: outcomeFromError(error instanceof SocialError ? error : undefined, target, observedAt());
|
|
498
504
|
}
|
|
499
505
|
if (claim?.kind === "new" && config.idempotencyStore !== undefined) {
|
|
500
506
|
try {
|
|
@@ -583,16 +589,9 @@ export function createSocial(config) {
|
|
|
583
589
|
const graphFacade = {
|
|
584
590
|
async getProfile(account, input, callOptions) {
|
|
585
591
|
validateAccountRef(account, "profiles.read");
|
|
586
|
-
if (input
|
|
587
|
-
|
|
588
|
-
(input.
|
|
589
|
-
input.handle !== undefined &&
|
|
590
|
-
typeof input.profileId !== "string" &&
|
|
591
|
-
typeof input.handle !== "string") ||
|
|
592
|
-
(input.profileId !== undefined &&
|
|
593
|
-
(typeof input.profileId !== "string" || input.profileId.trim() === "")) ||
|
|
594
|
-
(input.handle !== undefined &&
|
|
595
|
-
(typeof input.handle !== "string" || input.handle.trim() === "")))
|
|
592
|
+
if (!isObjectInput(input) ||
|
|
593
|
+
(input.profileId !== undefined && !isNonBlankStringInput(input.profileId)) ||
|
|
594
|
+
(input.handle !== undefined && !isNonBlankStringInput(input.handle)))
|
|
596
595
|
throw new SocialError({
|
|
597
596
|
code: "invalid_input",
|
|
598
597
|
operation: "profiles.read",
|
|
@@ -608,8 +607,7 @@ export function createSocial(config) {
|
|
|
608
607
|
},
|
|
609
608
|
async listRelationships(account, input, callOptions) {
|
|
610
609
|
validateAccountRef(account, "graph.read");
|
|
611
|
-
if (input
|
|
612
|
-
typeof input !== "object" ||
|
|
610
|
+
if (!isObjectInput(input) ||
|
|
613
611
|
!["following", "followers", "blocked", "muted"].includes(input.kind) ||
|
|
614
612
|
(input.limit !== undefined && (!Number.isSafeInteger(input.limit) || input.limit < 1)))
|
|
615
613
|
throw new SocialError({
|
|
@@ -636,30 +634,52 @@ export function createSocial(config) {
|
|
|
636
634
|
]);
|
|
637
635
|
const page = await dispatch(account.backend, callOptions?.signal, "graph.read", () => adapter.graph.listRelationships(account, {
|
|
638
636
|
...input,
|
|
639
|
-
...(
|
|
637
|
+
...definedFields({
|
|
638
|
+
cursor: input.cursor === undefined ? undefined : decodeCursor(scope, input.cursor),
|
|
639
|
+
}),
|
|
640
640
|
}, makeContext(account.backend, correlationId, callOptions)));
|
|
641
641
|
return encodePage(scope, page);
|
|
642
642
|
},
|
|
643
|
-
follow: (target, options) => graphMutation(target, "graph.follow", options)
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
643
|
+
follow: (target, options) => graphMutation(target, "graph.follow", options, async (graph, context) => {
|
|
644
|
+
if (graph.follow === undefined)
|
|
645
|
+
unsupported("graph.follow", target.backend);
|
|
646
|
+
return await graph.follow(target, context);
|
|
647
|
+
}),
|
|
648
|
+
unfollow: (target, options) => graphMutation(target, "graph.unfollow", options, async (graph, context) => {
|
|
649
|
+
if (graph.unfollow === undefined)
|
|
650
|
+
unsupported("graph.unfollow", target.backend);
|
|
651
|
+
await graph.unfollow(target, context);
|
|
652
|
+
}),
|
|
653
|
+
block: (target, options) => graphMutation(target, "graph.block", options, async (graph, context) => {
|
|
654
|
+
if (graph.block === undefined)
|
|
655
|
+
unsupported("graph.block", target.backend);
|
|
656
|
+
return await graph.block(target, context);
|
|
657
|
+
}),
|
|
658
|
+
unblock: (target, options) => graphMutation(target, "graph.unblock", options, async (graph, context) => {
|
|
659
|
+
if (graph.unblock === undefined)
|
|
660
|
+
unsupported("graph.unblock", target.backend);
|
|
661
|
+
await graph.unblock(target, context);
|
|
662
|
+
}),
|
|
663
|
+
mute: (target, options) => graphMutation(target, "graph.mute", options, async (graph, context) => {
|
|
664
|
+
if (graph.mute === undefined)
|
|
665
|
+
unsupported("graph.mute", target.backend);
|
|
666
|
+
return await graph.mute(target, context);
|
|
667
|
+
}),
|
|
668
|
+
unmute: (target, options) => graphMutation(target, "graph.unmute", options, async (graph, context) => {
|
|
669
|
+
if (graph.unmute === undefined)
|
|
670
|
+
unsupported("graph.unmute", target.backend);
|
|
671
|
+
await graph.unmute(target, context);
|
|
672
|
+
}),
|
|
649
673
|
};
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
674
|
+
/** Validates and authorizes a graph mutation, then runs `mutate` inside the backend limiter. */
|
|
675
|
+
async function graphMutation(target, operation, callOptions, mutate) {
|
|
676
|
+
if (!isObjectInput(target) ||
|
|
653
677
|
target.kind !== "profile" ||
|
|
654
678
|
target.version !== 1 ||
|
|
655
|
-
|
|
656
|
-
target.
|
|
657
|
-
|
|
658
|
-
target.
|
|
659
|
-
typeof target.accountId !== "string" ||
|
|
660
|
-
target.accountId.trim() === "" ||
|
|
661
|
-
typeof target.profileId !== "string" ||
|
|
662
|
-
target.profileId.trim() === "")
|
|
679
|
+
!isNonBlankStringInput(target.backend) ||
|
|
680
|
+
!isNonBlankStringInput(target.platform) ||
|
|
681
|
+
!isNonBlankStringInput(target.accountId) ||
|
|
682
|
+
!isNonBlankStringInput(target.profileId))
|
|
663
683
|
throw new SocialError({
|
|
664
684
|
code: "invalid_input",
|
|
665
685
|
operation,
|
|
@@ -679,37 +699,8 @@ export function createSocial(config) {
|
|
|
679
699
|
if (adapter.graph === undefined)
|
|
680
700
|
unsupported(operation, target.backend);
|
|
681
701
|
const context = makeContext(target.backend, correlationId, callOptions);
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
case "graph.follow":
|
|
685
|
-
if (adapter.graph.follow === undefined)
|
|
686
|
-
unsupported(operation, target.backend);
|
|
687
|
-
return await adapter.graph.follow(target, context);
|
|
688
|
-
case "graph.unfollow":
|
|
689
|
-
if (adapter.graph.unfollow === undefined)
|
|
690
|
-
unsupported(operation, target.backend);
|
|
691
|
-
await adapter.graph.unfollow(target, context);
|
|
692
|
-
return undefined;
|
|
693
|
-
case "graph.block":
|
|
694
|
-
if (adapter.graph.block === undefined)
|
|
695
|
-
unsupported(operation, target.backend);
|
|
696
|
-
return await adapter.graph.block(target, context);
|
|
697
|
-
case "graph.unblock":
|
|
698
|
-
if (adapter.graph.unblock === undefined)
|
|
699
|
-
unsupported(operation, target.backend);
|
|
700
|
-
await adapter.graph.unblock(target, context);
|
|
701
|
-
return undefined;
|
|
702
|
-
case "graph.mute":
|
|
703
|
-
if (adapter.graph.mute === undefined)
|
|
704
|
-
unsupported(operation, target.backend);
|
|
705
|
-
return await adapter.graph.mute(target, context);
|
|
706
|
-
case "graph.unmute":
|
|
707
|
-
if (adapter.graph.unmute === undefined)
|
|
708
|
-
unsupported(operation, target.backend);
|
|
709
|
-
await adapter.graph.unmute(target, context);
|
|
710
|
-
return undefined;
|
|
711
|
-
}
|
|
712
|
-
});
|
|
702
|
+
const graph = adapter.graph;
|
|
703
|
+
return dispatch(target.backend, callOptions?.signal, operation, () => mutate(graph, context));
|
|
713
704
|
}
|
|
714
705
|
async function lifecycle(ref, expectedKind, operation, callOptions) {
|
|
715
706
|
if (ref.kind !== expectedKind || ref.version !== 1 || !ref.accountId || !ref.platform)
|
|
@@ -780,7 +771,7 @@ export function createSocial(config) {
|
|
|
780
771
|
operation: "posts.publishSequence",
|
|
781
772
|
message: "A sequence requires at least one item",
|
|
782
773
|
});
|
|
783
|
-
if (
|
|
774
|
+
if (!isNonBlankStringInput(request.idempotencyKey))
|
|
784
775
|
throw new SocialError({
|
|
785
776
|
code: "invalid_input",
|
|
786
777
|
operation: "posts.publishSequence",
|
|
@@ -917,13 +908,10 @@ export function createSocial(config) {
|
|
|
917
908
|
validateAccountRef(account, "search.posts");
|
|
918
909
|
if (input === null ||
|
|
919
910
|
input === undefined ||
|
|
920
|
-
|
|
921
|
-
!input.query.trim() ||
|
|
911
|
+
!isNonBlankStringInput(input.query) ||
|
|
922
912
|
(input.limit !== undefined && (!Number.isSafeInteger(input.limit) || input.limit < 1)) ||
|
|
923
|
-
(input.startTime !== undefined &&
|
|
924
|
-
|
|
925
|
-
(input.endTime !== undefined &&
|
|
926
|
-
(typeof input.endTime !== "string" || !validTimestamp(input.endTime))) ||
|
|
913
|
+
(input.startTime !== undefined && !validTimestamp(input.startTime)) ||
|
|
914
|
+
(input.endTime !== undefined && !validTimestamp(input.endTime)) ||
|
|
927
915
|
(input.startTime !== undefined &&
|
|
928
916
|
input.endTime !== undefined &&
|
|
929
917
|
Date.parse(input.startTime) > Date.parse(input.endTime)) ||
|
|
@@ -1010,14 +998,10 @@ export function createSocial(config) {
|
|
|
1010
998
|
query.from > query.to ||
|
|
1011
999
|
!Array.isArray(query.metrics) ||
|
|
1012
1000
|
query.metrics.length === 0 ||
|
|
1013
|
-
query.metrics.some(
|
|
1014
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- runtime query boundary.
|
|
1015
|
-
(metric) => typeof metric !== "string" || !metric.trim()) ||
|
|
1001
|
+
query.metrics.some((metric) => !isNonBlankStringInput(metric)) ||
|
|
1016
1002
|
(query.dimensions !== undefined &&
|
|
1017
1003
|
(!Array.isArray(query.dimensions) ||
|
|
1018
|
-
query.dimensions.some(
|
|
1019
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- runtime query boundary.
|
|
1020
|
-
(dimension) => typeof dimension !== "string" || !dimension.trim()))))
|
|
1004
|
+
query.dimensions.some((dimension) => !isNonBlankStringInput(dimension)))))
|
|
1021
1005
|
throw new SocialError({
|
|
1022
1006
|
code: "invalid_input",
|
|
1023
1007
|
operation: "analytics.report.read",
|
|
@@ -1201,9 +1185,7 @@ export function createSocial(config) {
|
|
|
1201
1185
|
},
|
|
1202
1186
|
async markSeen(account, input = {}, callOptions) {
|
|
1203
1187
|
validateAccountRef(account, "notifications.seen");
|
|
1204
|
-
if (input
|
|
1205
|
-
typeof input !== "object" ||
|
|
1206
|
-
(input.seenAt !== undefined && !validTimestamp(input.seenAt)))
|
|
1188
|
+
if (!isObjectInput(input) || (input.seenAt !== undefined && !validTimestamp(input.seenAt)))
|
|
1207
1189
|
throw new SocialError({
|
|
1208
1190
|
code: "invalid_input",
|
|
1209
1191
|
operation: "notifications.seen",
|
|
@@ -1264,12 +1246,10 @@ export function createSocial(config) {
|
|
|
1264
1246
|
};
|
|
1265
1247
|
}
|
|
1266
1248
|
function decodePageOptions(scope, options) {
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
input.limit = options.limit;
|
|
1272
|
-
return input;
|
|
1249
|
+
return definedFields({
|
|
1250
|
+
cursor: options?.cursor === undefined ? undefined : decodeCursor(scope, options.cursor),
|
|
1251
|
+
limit: options?.limit,
|
|
1252
|
+
});
|
|
1273
1253
|
}
|
|
1274
1254
|
function encodePage(scope, page) {
|
|
1275
1255
|
const result = { ...page };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fields of `T` with `undefined` removed, each one optional. `T[K] & ({} | null)`
|
|
3
|
+
* is the form TypeScript narrows a generic `T[K]` to after an `!== undefined`
|
|
4
|
+
* check. For concrete types it matches `Exclude<T[K], undefined>`, except that an
|
|
5
|
+
* `unknown` field becomes `{} | null`.
|
|
6
|
+
*/
|
|
7
|
+
export type DefinedFields<T> = {
|
|
8
|
+
[K in keyof T]?: T[K] & ({} | null);
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Copy only the fields whose value is defined, so optional properties stay
|
|
12
|
+
* absent under `exactOptionalPropertyTypes` instead of being set to `undefined`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function definedFields<T extends object>(fields: T): DefinedFields<T>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy only the fields whose value is defined, so optional properties stay
|
|
3
|
+
* absent under `exactOptionalPropertyTypes` instead of being set to `undefined`.
|
|
4
|
+
*/
|
|
5
|
+
export function definedFields(fields) {
|
|
6
|
+
const result = {};
|
|
7
|
+
for (const key in fields) {
|
|
8
|
+
const value = fields[key];
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
continue;
|
|
11
|
+
result[key] = value;
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|