@opencoredev/social-sdk 0.1.1 → 0.2.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 (117) hide show
  1. package/README.md +10 -2
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cloud/common.d.ts.map +1 -1
  6. package/dist/cloud/common.js +48 -20
  7. package/dist/cloud/common.js.map +1 -1
  8. package/dist/cloud/lifecycle.d.ts.map +1 -1
  9. package/dist/cloud/lifecycle.js +5 -0
  10. package/dist/cloud/lifecycle.js.map +1 -1
  11. package/dist/cloud/media.d.ts.map +1 -1
  12. package/dist/cloud/media.js +12 -8
  13. package/dist/cloud/media.js.map +1 -1
  14. package/dist/cloud/outcomes.d.ts.map +1 -1
  15. package/dist/cloud/outcomes.js +17 -9
  16. package/dist/cloud/outcomes.js.map +1 -1
  17. package/dist/cloud/post-for-me.d.ts.map +1 -1
  18. package/dist/cloud/post-for-me.js +99 -28
  19. package/dist/cloud/post-for-me.js.map +1 -1
  20. package/dist/cloud/zernio.d.ts.map +1 -1
  21. package/dist/cloud/zernio.js +100 -9
  22. package/dist/cloud/zernio.js.map +1 -1
  23. package/dist/core/adapter.d.ts +42 -1
  24. package/dist/core/adapter.d.ts.map +1 -1
  25. package/dist/core/adapter.js.map +1 -1
  26. package/dist/core/client.d.ts +35 -1
  27. package/dist/core/client.d.ts.map +1 -1
  28. package/dist/core/client.js +503 -110
  29. package/dist/core/client.js.map +1 -1
  30. package/dist/core/concurrency.d.ts.map +1 -1
  31. package/dist/core/concurrency.js +5 -3
  32. package/dist/core/concurrency.js.map +1 -1
  33. package/dist/core/errors.d.ts.map +1 -1
  34. package/dist/core/errors.js +14 -7
  35. package/dist/core/errors.js.map +1 -1
  36. package/dist/core/idempotency.d.ts.map +1 -1
  37. package/dist/core/idempotency.js.map +1 -1
  38. package/dist/core/index.d.ts.map +1 -1
  39. package/dist/core/index.js.map +1 -1
  40. package/dist/core/pagination.d.ts.map +1 -1
  41. package/dist/core/pagination.js +1 -0
  42. package/dist/core/pagination.js.map +1 -1
  43. package/dist/core/types.d.ts +70 -2
  44. package/dist/core/types.d.ts.map +1 -1
  45. package/dist/core/types.js +10 -0
  46. package/dist/core/types.js.map +1 -1
  47. package/dist/platforms/bluesky.d.ts +131 -1
  48. package/dist/platforms/bluesky.d.ts.map +1 -1
  49. package/dist/platforms/bluesky.js +772 -44
  50. package/dist/platforms/bluesky.js.map +1 -1
  51. package/dist/platforms/instagram.d.ts +70 -3
  52. package/dist/platforms/instagram.d.ts.map +1 -1
  53. package/dist/platforms/instagram.js +403 -47
  54. package/dist/platforms/instagram.js.map +1 -1
  55. package/dist/platforms/linkedin.d.ts +55 -0
  56. package/dist/platforms/linkedin.d.ts.map +1 -1
  57. package/dist/platforms/linkedin.js +350 -39
  58. package/dist/platforms/linkedin.js.map +1 -1
  59. package/dist/platforms/threads.d.ts +37 -4
  60. package/dist/platforms/threads.d.ts.map +1 -1
  61. package/dist/platforms/threads.js +419 -44
  62. package/dist/platforms/threads.js.map +1 -1
  63. package/dist/platforms/tiktok.d.ts.map +1 -1
  64. package/dist/platforms/tiktok.js +109 -26
  65. package/dist/platforms/tiktok.js.map +1 -1
  66. package/dist/platforms/x-engagement.d.ts.map +1 -1
  67. package/dist/platforms/x-engagement.js +4 -0
  68. package/dist/platforms/x-engagement.js.map +1 -1
  69. package/dist/platforms/x.d.ts +227 -9
  70. package/dist/platforms/x.d.ts.map +1 -1
  71. package/dist/platforms/x.js +826 -49
  72. package/dist/platforms/x.js.map +1 -1
  73. package/dist/platforms/youtube-upload.d.ts.map +1 -1
  74. package/dist/platforms/youtube-upload.js +25 -9
  75. package/dist/platforms/youtube-upload.js.map +1 -1
  76. package/dist/platforms/youtube.d.ts +56 -2
  77. package/dist/platforms/youtube.d.ts.map +1 -1
  78. package/dist/platforms/youtube.js +571 -31
  79. package/dist/platforms/youtube.js.map +1 -1
  80. package/dist/server/connections.d.ts.map +1 -1
  81. package/dist/server/connections.js +4 -2
  82. package/dist/server/connections.js.map +1 -1
  83. package/dist/server/credentials.d.ts.map +1 -1
  84. package/dist/server/credentials.js +10 -16
  85. package/dist/server/credentials.js.map +1 -1
  86. package/dist/server/index.d.ts.map +1 -1
  87. package/dist/server/index.js.map +1 -1
  88. package/dist/server/oauth.d.ts +5 -0
  89. package/dist/server/oauth.d.ts.map +1 -1
  90. package/dist/server/oauth.js +118 -47
  91. package/dist/server/oauth.js.map +1 -1
  92. package/dist/server/webhooks.d.ts.map +1 -1
  93. package/dist/server/webhooks.js +11 -4
  94. package/dist/server/webhooks.js.map +1 -1
  95. package/dist/testing/index.d.ts.map +1 -1
  96. package/dist/testing/index.js +153 -1
  97. package/dist/testing/index.js.map +1 -1
  98. package/dist/transport/binary.d.ts.map +1 -1
  99. package/dist/transport/binary.js.map +1 -1
  100. package/dist/transport/budget.d.ts.map +1 -1
  101. package/dist/transport/budget.js.map +1 -1
  102. package/dist/transport/http.d.ts +2 -1
  103. package/dist/transport/http.d.ts.map +1 -1
  104. package/dist/transport/http.js +42 -12
  105. package/dist/transport/http.js.map +1 -1
  106. package/dist/transport/json.d.ts +7 -1
  107. package/dist/transport/json.d.ts.map +1 -1
  108. package/dist/transport/json.js +7 -1
  109. package/dist/transport/json.js.map +1 -1
  110. package/dist/transport/upload.d.ts +1 -0
  111. package/dist/transport/upload.d.ts.map +1 -1
  112. package/dist/transport/upload.js +38 -17
  113. package/dist/transport/upload.js.map +1 -1
  114. package/dist/transport/validation.d.ts.map +1 -1
  115. package/dist/transport/validation.js +1 -0
  116. package/dist/transport/validation.js.map +1 -1
  117. package/package.json +2 -1
@@ -17,6 +17,7 @@ export function linkedin(options) {
17
17
  }
18
18
  const http = createHttp(options.fetch ? { fetch: options.fetch } : {});
19
19
  const now = () => (options.clock?.() ?? new Date()).toISOString();
20
+ const escapeCommentary = (text) => text.replace(/[|{}@()[\]<>#\\*_~]/g, "\\$&");
20
21
  const authorize = (ref, context) => {
21
22
  if (ref.backend !== context.backendInstance ||
22
23
  ref.platform !== "linkedin" ||
@@ -27,7 +28,7 @@ export function linkedin(options) {
27
28
  message: "Reference does not belong to this LinkedIn author authorization.",
28
29
  });
29
30
  };
30
- async function request(path, context, body, selectedHeaders, method = body === undefined ? "GET" : "POST") {
31
+ async function request(path, context, body, selectedHeaders, method = body === undefined ? "GET" : "POST", extraHeaders) {
31
32
  try {
32
33
  return await http({
33
34
  timeoutMs: remainingBudget(context),
@@ -37,10 +38,14 @@ export function linkedin(options) {
37
38
  "Content-Type": "application/json",
38
39
  "Linkedin-Version": options.apiVersion,
39
40
  "X-Restli-Protocol-Version": "2.0.0",
41
+ ...extraHeaders,
40
42
  },
41
43
  method,
44
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
42
45
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
46
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
43
47
  ...(context.signal ? { signal: context.signal } : {}),
48
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
44
49
  ...(selectedHeaders ? { responseHeaders: selectedHeaders } : {}),
45
50
  maxAttempts: method === "GET" ? Math.min(5, context.retryBudget.maxAttempts) : 1,
46
51
  });
@@ -54,17 +59,27 @@ export function linkedin(options) {
54
59
  throw new SocialError({
55
60
  code: ambiguous
56
61
  ? "ambiguous_outcome"
57
- : error.status === 401
58
- ? "reconnect_required"
59
- : error.status === 403
60
- ? "missing_permission"
61
- : error.status === 429
62
- ? "rate_limited"
63
- : "upstream_failure",
62
+ : error.kind === "timeout"
63
+ ? "timeout"
64
+ : error.kind === "cancelled"
65
+ ? "cancelled"
66
+ : error.status === 401
67
+ ? "reconnect_required"
68
+ : error.status === 403
69
+ ? "missing_permission"
70
+ : error.status === 404
71
+ ? "not_found"
72
+ : error.status === 429
73
+ ? "rate_limited"
74
+ : "upstream_failure",
64
75
  operation: path,
65
76
  message: error.message,
66
77
  upstreamStatus: error.status,
67
- retryDisposition: ambiguous ? { kind: "reconcile-first" } : { kind: "never" },
78
+ retryDisposition: ambiguous
79
+ ? { kind: "reconcile-first" }
80
+ : error.status === 429 && error.retryAfterMs !== undefined
81
+ ? { kind: "after-delay", delayMs: error.retryAfterMs }
82
+ : { kind: "never" },
68
83
  });
69
84
  }
70
85
  }
@@ -116,12 +131,15 @@ export function linkedin(options) {
116
131
  url: string(initialized["uploadUrl"]),
117
132
  source: {
118
133
  mimeType: media.mimeType,
134
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
119
135
  ...(size === undefined ? {} : { size }),
120
136
  open: source.kind === "blob" ? () => source.blob.stream() : source.open,
121
137
  },
122
138
  allowHost: (host) => host === "www.linkedin.com",
123
139
  maxBytes: 20 * 1024 * 1024,
140
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
124
141
  ...(options.fetch ? { fetch: options.fetch } : {}),
142
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
125
143
  ...(context.signal ? { signal: context.signal } : {}),
126
144
  });
127
145
  return {
@@ -133,6 +151,208 @@ export function linkedin(options) {
133
151
  mediaId,
134
152
  };
135
153
  }
154
+ /* oxlint-disable anti-slop/no-unknown-parameters, anti-slop/no-unsafe-dictionary-type, anti-slop/no-known-value-widening, anti-slop/no-conditional-empty-object-spread, anti-slop/no-runtime-typeof, anti-slop/require-readable-spacing -- These helpers normalize documented LinkedIn response facets at the transport boundary. */
155
+ const organizationOnly = (account, context, operation) => {
156
+ authorize(account, context);
157
+ if (!account.accountId.startsWith("urn:li:organization:"))
158
+ throw new SocialError({
159
+ code: "unsupported_capability",
160
+ operation,
161
+ message: "LinkedIn organization analytics require an organization account, not a member account.",
162
+ });
163
+ };
164
+ const statisticsPath = (path, finder, account, interval) => {
165
+ const params = [`q=${finder}`, `${finder}=${encodeURIComponent(account.accountId)}`];
166
+ if (interval) {
167
+ if (interval.start !== undefined &&
168
+ (!Number.isSafeInteger(interval.start) || interval.start < 0))
169
+ throw new SocialError({
170
+ code: "invalid_input",
171
+ operation: "analytics.organization.read",
172
+ message: "Interval start must be a nonnegative epoch-millisecond integer.",
173
+ });
174
+ if (interval.end !== undefined && (!Number.isSafeInteger(interval.end) || interval.end < 0))
175
+ throw new SocialError({
176
+ code: "invalid_input",
177
+ operation: "analytics.organization.read",
178
+ message: "Interval end must be a nonnegative epoch-millisecond integer.",
179
+ });
180
+ if (interval.start !== undefined &&
181
+ interval.end !== undefined &&
182
+ interval.end <= interval.start)
183
+ throw new SocialError({
184
+ code: "invalid_input",
185
+ operation: "analytics.organization.read",
186
+ message: "Interval end must be after interval start.",
187
+ });
188
+ if (interval.start === undefined)
189
+ throw new SocialError({
190
+ code: "invalid_input",
191
+ operation: "analytics.organization.read",
192
+ message: "Interval start is required for time-bound organization statistics.",
193
+ });
194
+ const rangeParts = [
195
+ `start:${interval.start}`,
196
+ ...(interval.end === undefined ? [] : [`end:${interval.end}`]),
197
+ ];
198
+ const range = `(timeRange:(${rangeParts.join(",")}),timeGranularityType:${interval.granularity})`;
199
+ if (!["DAY", "WEEK", "MONTH"].includes(interval.granularity))
200
+ throw new SocialError({
201
+ code: "invalid_input",
202
+ operation: "analytics.organization.read",
203
+ message: "Interval granularity must be DAY, WEEK or MONTH.",
204
+ });
205
+ params.push(`timeIntervals=${range}`);
206
+ }
207
+ return `${path}?${params.join("&")}`;
208
+ };
209
+ const numberMap = (value) => {
210
+ const row = value === undefined ? {} : object(value);
211
+ const output = {};
212
+ for (const [key, item] of Object.entries(row)) {
213
+ const number = optionalNumber(item);
214
+ if (number !== undefined)
215
+ output[key] = number;
216
+ else if (item !== null && typeof item === "object" && !Array.isArray(item)) {
217
+ const nestedObject = object(item);
218
+ const nested = ["pageViews", "uniquePageViews", "clicks", "count"]
219
+ .map((name) => optionalNumber(nestedObject[name]))
220
+ .find((candidate) => candidate !== undefined);
221
+ if (nested !== undefined)
222
+ output[key] = nested;
223
+ else {
224
+ for (const [nestedKey, nestedValue] of Object.entries(nestedObject)) {
225
+ const nestedNumber = optionalNumber(nestedValue);
226
+ if (nestedNumber !== undefined)
227
+ output[`${key}.${nestedKey}`] = nestedNumber;
228
+ }
229
+ }
230
+ }
231
+ }
232
+ return output;
233
+ };
234
+ const followerBreakdowns = (row) => {
235
+ const dimensions = [
236
+ ["function", "followerCountsByFunction", "function"],
237
+ ["seniority", "followerCountsBySeniority", "seniority"],
238
+ ["industry", "followerCountsByIndustry", "industry"],
239
+ ["geo", "followerCountsByGeo", "geo"],
240
+ ["geoCountry", "followerCountsByGeoCountry", "geo"],
241
+ ["staffCountRange", "followerCountsByStaffCountRange", "staffCountRange"],
242
+ ["associationType", "followerCountsByAssociationType", "associationType"],
243
+ ];
244
+ const output = [];
245
+ for (const [dimension, field, key] of dimensions) {
246
+ if (row[field] === undefined)
247
+ continue;
248
+ for (const value of array(row[field])) {
249
+ const item = object(value);
250
+ const label = optionalString(item[key]);
251
+ if (!label)
252
+ continue;
253
+ const counts = item["followerCounts"] === undefined ? {} : object(item["followerCounts"]);
254
+ output.push({
255
+ dimension,
256
+ value: label,
257
+ ...(optionalNumber(counts["organicFollowerCount"]) === undefined
258
+ ? {}
259
+ : { organicFollowerCount: optionalNumber(counts["organicFollowerCount"]) }),
260
+ ...(optionalNumber(counts["paidFollowerCount"]) === undefined
261
+ ? {}
262
+ : { paidFollowerCount: optionalNumber(counts["paidFollowerCount"]) }),
263
+ });
264
+ }
265
+ }
266
+ return output;
267
+ };
268
+ const parseInterval = (row, requestedGranularity) => {
269
+ if (row["timeRange"] === undefined)
270
+ return undefined;
271
+ const range = object(row["timeRange"]);
272
+ const start = optionalNumber(range["start"]);
273
+ const end = optionalNumber(range["end"]);
274
+ const granularity = requestedGranularity ?? optionalString(row["timeGranularityType"]);
275
+ if ((granularity !== "DAY" && granularity !== "WEEK" && granularity !== "MONTH") ||
276
+ (start === undefined && end === undefined))
277
+ return undefined;
278
+ return {
279
+ granularity,
280
+ ...(start === undefined ? {} : { start }),
281
+ ...(end === undefined ? {} : { end }),
282
+ };
283
+ };
284
+ const parseFollowerStatistics = (result, requestedGranularity) => array(result["elements"]).map((value) => {
285
+ const row = object(value);
286
+ const gains = row["followerGains"] === undefined ? {} : object(row["followerGains"]);
287
+ return {
288
+ organization: string(row["organizationalEntity"]),
289
+ ...(parseInterval(row, requestedGranularity) === undefined
290
+ ? {}
291
+ : { interval: parseInterval(row, requestedGranularity) }),
292
+ ...(optionalNumber(gains["organicFollowerGain"]) === undefined
293
+ ? {}
294
+ : { organicFollowerGain: optionalNumber(gains["organicFollowerGain"]) }),
295
+ ...(optionalNumber(gains["paidFollowerGain"]) === undefined
296
+ ? {}
297
+ : { paidFollowerGain: optionalNumber(gains["paidFollowerGain"]) }),
298
+ breakdowns: followerBreakdowns(row),
299
+ };
300
+ });
301
+ const pageBreakdowns = (row) => {
302
+ const output = [];
303
+ for (const [field, dimension, key] of [
304
+ ["pageStatisticsByFunction", "function", "function"],
305
+ ["pageStatisticsBySeniority", "seniority", "seniority"],
306
+ ["pageStatisticsByIndustryV2", "industryV2", "industryV2"],
307
+ ["pageStatisticsByIndustry", "industry", "industry"],
308
+ ["pageStatisticsByGeo", "geo", "geo"],
309
+ ["pageStatisticsByGeoCountry", "geoCountry", "geo"],
310
+ ["pageStatisticsByStaffCountRange", "staffCountRange", "staffCountRange"],
311
+ ]) {
312
+ if (row[field] === undefined)
313
+ continue;
314
+ for (const value of array(row[field])) {
315
+ const item = object(value);
316
+ const label = optionalString(item[key]);
317
+ if (!label)
318
+ continue;
319
+ const stats = item["pageStatistics"] === undefined ? {} : object(item["pageStatistics"]);
320
+ const views = stats["views"] === undefined ? {} : object(stats["views"]);
321
+ const clicks = stats["clicks"] === undefined ? {} : object(stats["clicks"]);
322
+ output.push({
323
+ dimension,
324
+ value: label,
325
+ views: numberMap(views),
326
+ clicks: numberMap(clicks),
327
+ });
328
+ }
329
+ }
330
+ return output;
331
+ };
332
+ const parsePageStatistics = (result, requestedGranularity) => array(result["elements"]).map((value) => {
333
+ const row = object(value);
334
+ const total = row["totalPageStatistics"] === undefined ? {} : object(row["totalPageStatistics"]);
335
+ return {
336
+ organization: string(row["organization"]),
337
+ ...(parseInterval(row, requestedGranularity) === undefined
338
+ ? {}
339
+ : { interval: parseInterval(row, requestedGranularity) }),
340
+ views: numberMap(total["views"]),
341
+ clicks: numberMap(total["clicks"]),
342
+ breakdowns: pageBreakdowns(row),
343
+ };
344
+ });
345
+ const parseShareStatistics = (result, requestedGranularity) => array(result["elements"]).map((value) => {
346
+ const row = object(value);
347
+ return {
348
+ organization: string(row["organizationalEntity"]),
349
+ ...(parseInterval(row, requestedGranularity) === undefined
350
+ ? {}
351
+ : { interval: parseInterval(row, requestedGranularity) }),
352
+ metrics: numberMap(row["totalShareStatistics"]),
353
+ };
354
+ });
355
+ /* oxlint-enable anti-slop/no-unknown-parameters, anti-slop/no-unsafe-dictionary-type, anti-slop/no-known-value-widening, anti-slop/no-conditional-empty-object-spread, anti-slop/no-runtime-typeof */
136
356
  return defineAdapter({
137
357
  id: "linkedin",
138
358
  capabilities: {
@@ -167,25 +387,38 @@ export function linkedin(options) {
167
387
  {
168
388
  platform: "linkedin",
169
389
  operation: "posts.multi-image",
170
- availability: "available",
390
+ availability: "not-implemented-by-adapter",
171
391
  formats: ["carousel"],
172
392
  },
173
393
  {
174
394
  platform: "linkedin",
175
395
  operation: "posts.video",
176
- availability: "available",
396
+ availability: "not-implemented-by-adapter",
177
397
  formats: ["video"],
178
398
  },
179
- { platform: "linkedin", operation: "posts.document", availability: "available" },
399
+ {
400
+ platform: "linkedin",
401
+ operation: "posts.document",
402
+ availability: "not-implemented-by-adapter",
403
+ },
180
404
  { platform: "linkedin", operation: "polls.create", availability: "available" },
181
405
  { platform: "linkedin", operation: "reactions.write", availability: "available" },
182
406
  { platform: "linkedin", operation: "reshares.write", availability: "available" },
183
407
  { platform: "linkedin", operation: "posts.update", availability: "available" },
184
408
  { platform: "linkedin", operation: "posts.delete", availability: "available" },
409
+ {
410
+ platform: "linkedin",
411
+ operation: "posts.removeFromPlatform",
412
+ availability: "available",
413
+ },
185
414
  {
186
415
  platform: "linkedin",
187
416
  operation: "analytics.organization.read",
188
- availability: "permission-required",
417
+ availability: options.auth.author.startsWith("urn:li:organization:")
418
+ ? "available"
419
+ : "account-ineligible",
420
+ requiredScopes: ["rw_organization_admin"],
421
+ notes: "Requires the Community Management API product and organization administrator access. Member accounts are not eligible.",
189
422
  },
190
423
  {
191
424
  platform: "linkedin",
@@ -206,6 +439,15 @@ export function linkedin(options) {
206
439
  requiredScopes: ["rw_organization_admin"],
207
440
  notes: "Organization total followers only; authenticated member must administer the organization. No member-profile analytics claimed.",
208
441
  },
442
+ ...["analytics.followers.read", "analytics.page.read", "analytics.shares.read"].map((operation) => ({
443
+ platform: "linkedin",
444
+ operation,
445
+ availability: options.auth.author.startsWith("urn:li:organization:")
446
+ ? "available"
447
+ : "account-ineligible",
448
+ requiredScopes: ["rw_organization_admin"],
449
+ notes: "Community Management API product and organization administrator access are required. Member accounts are not eligible.",
450
+ })),
209
451
  {
210
452
  platform: "linkedin",
211
453
  operation: "posts.list",
@@ -215,7 +457,7 @@ export function linkedin(options) {
215
457
  ? "r_organization_social"
216
458
  : "r_member_social",
217
459
  ],
218
- notes: "Author feed requires approved read access for the configured author.",
460
+ notes: "Author-feed pagination uses the returned offset cursor.",
219
461
  },
220
462
  {
221
463
  platform: "linkedin",
@@ -227,6 +469,9 @@ export function linkedin(options) {
227
469
  platform: "linkedin",
228
470
  operation,
229
471
  availability: "available",
472
+ ...(operation === "analytics.read"
473
+ ? { requiredScopes: ["r_member_social"] }
474
+ : { requiredScopes: ["w_member_social", "r_member_social"] }),
230
475
  notes: "Community Management product permissions and author post read access required. Analytics contains returned social-action counts only.",
231
476
  })),
232
477
  ],
@@ -269,14 +514,24 @@ export function linkedin(options) {
269
514
  let content;
270
515
  if (media?.source.kind === "media-ref") {
271
516
  authorize(media.source.ref, context);
272
- const image = object(await request(`/rest/images/${encodeURIComponent(media.source.ref.mediaId)}`, context));
273
- if (image["owner"] !== target.account.accountId)
517
+ let image;
518
+ try {
519
+ // SAFETY: object() validates the upstream response as a JSON object.
520
+ image = object(await request(`/rest/images/${encodeURIComponent(media.source.ref.mediaId)}`, context));
521
+ }
522
+ catch (error) {
523
+ if (!(error instanceof SocialError) ||
524
+ error.code !== "missing_permission" ||
525
+ !options.auth.author.startsWith("urn:li:person:"))
526
+ throw error;
527
+ }
528
+ if (image?.["owner"] !== undefined && image["owner"] !== target.account.accountId)
274
529
  throw new SocialError({
275
530
  code: "unauthorized",
276
531
  operation: "posts.publish",
277
532
  message: "LinkedIn image belongs to a different author.",
278
533
  });
279
- if (image["status"] !== "AVAILABLE")
534
+ if (image?.["status"] !== undefined && image["status"] !== "AVAILABLE")
280
535
  throw new SocialError({
281
536
  code: "media_error",
282
537
  operation: "posts.publish",
@@ -285,13 +540,14 @@ export function linkedin(options) {
285
540
  content = {
286
541
  media: {
287
542
  id: media.source.ref.mediaId,
543
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
288
544
  ...(media.altText ? { altText: media.altText } : {}),
289
545
  },
290
546
  };
291
547
  }
292
548
  const result = object(await request("/rest/posts", context, {
293
549
  author: target.account.accountId,
294
- commentary: target.content.text ?? "",
550
+ commentary: escapeCommentary(target.content.text ?? ""),
295
551
  visibility: "PUBLIC",
296
552
  distribution: {
297
553
  feedDistribution: "MAIN_FEED",
@@ -300,6 +556,7 @@ export function linkedin(options) {
300
556
  },
301
557
  lifecycleState: "PUBLISHED",
302
558
  isReshareDisabledByAuthor: false,
559
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
303
560
  ...(content ? { content } : {}),
304
561
  }, ["x-restli-id"]));
305
562
  const id = optionalString(object(result["headers"])["x-restli-id"]);
@@ -376,11 +633,16 @@ export function linkedin(options) {
376
633
  const hasNext = array(paging["links"] ?? []).some((link) => object(link)["rel"] === "next");
377
634
  return {
378
635
  items,
636
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
379
637
  ...(items.length > 0 && (hasNext || (total !== undefined && start + items.length < total))
380
638
  ? { nextCursor: String(start + items.length) }
381
639
  : {}),
382
640
  };
383
641
  },
642
+ async removeFromPlatform(ref, context) {
643
+ authorize(ref, context);
644
+ await request(`/rest/posts/${encodeURIComponent(ref.postId)}`, context, undefined, undefined, "DELETE");
645
+ },
384
646
  },
385
647
  comments: {
386
648
  async list(ref, input, context) {
@@ -410,6 +672,7 @@ export function linkedin(options) {
410
672
  const next = total !== undefined && start + items.length < total
411
673
  ? String(start + items.length)
412
674
  : undefined;
675
+ // oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
413
676
  return { items, ...(next === undefined ? {} : { nextCursor: next }) };
414
677
  },
415
678
  async reply(ref, content, context) {
@@ -420,21 +683,24 @@ export function linkedin(options) {
420
683
  message: "Provide a comment of 1 to 1,250 characters.",
421
684
  });
422
685
  await readCommentablePost({ ...ref, kind: "platform-post" }, context);
423
- const match = /^urn:li:comment:\(urn:li:activity:\d+,(\d+)\)$/.exec(ref.commentId);
686
+ const match = /^urn:li:comment:\(urn:li:activity:(\d+),(\d+)\)$/.exec(ref.commentId);
424
687
  if (!match)
425
688
  throw new SocialError({
426
689
  code: "invalid_input",
427
690
  operation: "comments.write",
428
691
  message: "Use the complete commentUrn returned by LinkedIn comment reads.",
429
692
  });
430
- const parent = object(await request(`/rest/socialActions/${encodeURIComponent(ref.postId)}/comments/${match[1]}`, context));
693
+ const parent = object(await request(`/rest/socialActions/${encodeURIComponent(ref.postId)}/comments/${match[2]}`, context));
431
694
  if (parent["commentUrn"] !== ref.commentId)
432
695
  throw new SocialError({
433
696
  code: "unauthorized",
434
697
  operation: "comments.write",
435
698
  message: "Comment does not belong to the supplied post.",
436
699
  });
437
- if (typeof parent["object"] === "string" && parent["object"] !== ref.postId)
700
+ const parentObject = optionalString(parent["object"]);
701
+ if (parentObject !== undefined &&
702
+ parentObject !== ref.postId &&
703
+ parentObject !== `urn:li:activity:${match[1]}`)
438
704
  throw new SocialError({
439
705
  code: "unauthorized",
440
706
  operation: "comments.write",
@@ -525,46 +791,73 @@ export function linkedin(options) {
525
791
  });
526
792
  return publicFields(image, ["id", "owner", "status"]);
527
793
  },
528
- async registerVideo({ account, byteSize, context }) {
794
+ async registerVideo({ account, context }) {
529
795
  authorize(account, context);
530
- return (await request("/rest/videos?action=initializeUpload", context, {
531
- initializeUploadRequest: { owner: account.accountId, fileSizeBytes: byteSize },
532
- }));
796
+ throw new SocialError({
797
+ code: "unsupported_capability",
798
+ operation: "posts.video",
799
+ message: "LinkedIn video publishing is not implemented by this adapter.",
800
+ });
533
801
  },
534
- async createPoll({ account, text, options: pollOptions, context }) {
802
+ async createPoll({ account, text, options: pollOptions, duration = "THREE_DAYS", context }) {
535
803
  authorize(account, context);
536
- return (await request("/rest/posts", context, {
804
+ const result = object(await request("/rest/posts", context, {
537
805
  author: account.accountId,
538
- commentary: text,
539
- distribution: { feedDistribution: "MAIN_FEED" },
540
- content: { poll: { question: text, options: [...pollOptions] } },
541
- }));
806
+ commentary: escapeCommentary(text),
807
+ visibility: "PUBLIC",
808
+ distribution: {
809
+ feedDistribution: "MAIN_FEED",
810
+ targetEntities: [],
811
+ thirdPartyDistributionChannels: [],
812
+ },
813
+ lifecycleState: "PUBLISHED",
814
+ content: {
815
+ poll: {
816
+ question: text,
817
+ options: pollOptions.map((option) => ({ text: option })),
818
+ settings: { duration },
819
+ },
820
+ },
821
+ }, ["x-restli-id"]));
822
+ const id = optionalString(object(result["headers"])["x-restli-id"]);
823
+ // SAFETY: result is validated as a JSON object and id is a JSON string.
824
+ return (id === undefined ? result : { ...result, id });
542
825
  },
543
826
  async react({ account, postId, reaction, context }) {
544
827
  authorize(account, context);
545
- await request(`/rest/reactions`, context, {
828
+ await request(`/rest/reactions?actor=${encodeURIComponent(account.accountId)}`, context, {
546
829
  root: postId,
547
830
  reactionType: reaction,
548
- actor: account.accountId,
549
831
  });
550
832
  },
551
833
  async reshare({ account, postId, context }) {
552
834
  authorize(account, context);
553
- return (await request("/rest/posts", context, {
835
+ const result = object(await request("/rest/posts", context, {
554
836
  author: account.accountId,
555
- resharedPost: postId,
556
837
  commentary: "",
557
- }));
838
+ visibility: "PUBLIC",
839
+ distribution: {
840
+ feedDistribution: "MAIN_FEED",
841
+ targetEntities: [],
842
+ thirdPartyDistributionChannels: [],
843
+ },
844
+ lifecycleState: "PUBLISHED",
845
+ reshareContext: { parent: postId },
846
+ }, ["x-restli-id"]));
847
+ const id = optionalString(object(result["headers"])["x-restli-id"]);
848
+ // SAFETY: result is validated as a JSON object and id is a JSON string.
849
+ return (id === undefined ? result : { ...result, id });
558
850
  },
559
851
  async updatePost({ account, postId, body, context }) {
560
852
  authorize(account, context);
561
- return (await request(`/rest/posts/${encodeURIComponent(postId)}`, context, body, undefined, "PUT"));
853
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
854
+ return (await request(`/rest/posts/${encodeURIComponent(postId)}`, context, { patch: { $set: body } }, ["x-restli-id"], "POST", { "X-RestLi-Method": "PARTIAL_UPDATE" }));
562
855
  },
563
856
  async deletePost({ account, postId, context }) {
564
857
  authorize(account, context);
565
858
  await request(`/rest/posts/${encodeURIComponent(postId)}`, context, undefined, undefined, "DELETE");
566
859
  },
567
- async organizationAnalytics({ account, query, context }) {
860
+ async organizationAnalytics({ account, context }) {
568
861
  authorize(account, context);
569
862
  if (!account.accountId.startsWith("urn:li:organization:"))
570
863
  throw new SocialError({
@@ -572,7 +865,25 @@ export function linkedin(options) {
572
865
  operation: "analytics.organization.read",
573
866
  message: "Organization analytics requires an organization author.",
574
867
  });
575
- return (await request(`/rest/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=${encodeURIComponent(account.accountId)}`, context, query));
868
+ // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
869
+ return (await request(`/rest/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=${encodeURIComponent(account.accountId)}`, context));
870
+ },
871
+ async getOrganizationFollowerStatistics({ account, interval, context }) {
872
+ organizationOnly(account, context, "analytics.followers.read");
873
+ return parseFollowerStatistics(object(await request(statisticsPath("/rest/organizationalEntityFollowerStatistics", "organizationalEntity", account, interval), context)), interval?.granularity);
874
+ },
875
+ async getOrganizationPageStatistics({ account, interval, context }) {
876
+ organizationOnly(account, context, "analytics.page.read");
877
+ return parsePageStatistics(object(await request(statisticsPath("/rest/organizationPageStatistics", "organization", account, interval), context)), interval?.granularity);
878
+ },
879
+ async getOrganizationShareStatistics({ account, interval, context }) {
880
+ organizationOnly(account, context, "analytics.shares.read");
881
+ return parseShareStatistics(object(await request(statisticsPath("/rest/organizationalEntityShareStatistics", "organizationalEntity", account, interval), context)), interval?.granularity);
882
+ },
883
+ async getOrganizationFollowerCount({ account, context }) {
884
+ organizationOnly(account, context, "analytics.account.read");
885
+ const response = object(await request(`/rest/networkSizes/${encodeURIComponent(account.accountId)}?edgeType=COMPANY_FOLLOWED_BY_MEMBER`, context));
886
+ return optionalNumber(response["firstDegreeSize"]);
576
887
  },
577
888
  },
578
889
  });