@classytic/social 0.1.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 (121) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +368 -0
  4. package/dist/base-Bw7e52V8.mjs +246 -0
  5. package/dist/base-Bw7e52V8.mjs.map +1 -0
  6. package/dist/base-DBtKFiSX.d.mts +226 -0
  7. package/dist/base-DBtKFiSX.d.mts.map +1 -0
  8. package/dist/chunk-DQk6qfdC.mjs +18 -0
  9. package/dist/client/index.d.mts +44 -0
  10. package/dist/client/index.d.mts.map +1 -0
  11. package/dist/client/index.mjs +154 -0
  12. package/dist/client/index.mjs.map +1 -0
  13. package/dist/common/index.d.mts +3 -0
  14. package/dist/common/index.mjs +7 -0
  15. package/dist/contracts-Cdwa4zlg.d.mts +121 -0
  16. package/dist/contracts-Cdwa4zlg.d.mts.map +1 -0
  17. package/dist/contracts-lCa069IK.mjs +221 -0
  18. package/dist/contracts-lCa069IK.mjs.map +1 -0
  19. package/dist/env-Bl0cwwjC.mjs +955 -0
  20. package/dist/env-Bl0cwwjC.mjs.map +1 -0
  21. package/dist/env-DxOZHf0p.d.mts +394 -0
  22. package/dist/env-DxOZHf0p.d.mts.map +1 -0
  23. package/dist/errors-Cm6LeKf7.mjs +32 -0
  24. package/dist/errors-Cm6LeKf7.mjs.map +1 -0
  25. package/dist/facebook-l_4CghaA.mjs +95 -0
  26. package/dist/facebook-l_4CghaA.mjs.map +1 -0
  27. package/dist/http-DpcLSR1M.mjs +197 -0
  28. package/dist/http-DpcLSR1M.mjs.map +1 -0
  29. package/dist/index.d.mts +42 -0
  30. package/dist/index.d.mts.map +1 -0
  31. package/dist/index.mjs +71 -0
  32. package/dist/index.mjs.map +1 -0
  33. package/dist/instagram-BGaeUFU2.mjs +90 -0
  34. package/dist/instagram-BGaeUFU2.mjs.map +1 -0
  35. package/dist/linkedin-70whtVKa.mjs +101 -0
  36. package/dist/linkedin-70whtVKa.mjs.map +1 -0
  37. package/dist/meta-D3vcJU1c.mjs +126 -0
  38. package/dist/meta-D3vcJU1c.mjs.map +1 -0
  39. package/dist/pkce-jq5II68b.mjs +72 -0
  40. package/dist/pkce-jq5II68b.mjs.map +1 -0
  41. package/dist/polling-DZ1apXtA.mjs +25 -0
  42. package/dist/polling-DZ1apXtA.mjs.map +1 -0
  43. package/dist/providers/facebook.d.mts +135 -0
  44. package/dist/providers/facebook.d.mts.map +1 -0
  45. package/dist/providers/facebook.mjs +450 -0
  46. package/dist/providers/facebook.mjs.map +1 -0
  47. package/dist/providers/instagram.d.mts +122 -0
  48. package/dist/providers/instagram.d.mts.map +1 -0
  49. package/dist/providers/instagram.mjs +496 -0
  50. package/dist/providers/instagram.mjs.map +1 -0
  51. package/dist/providers/linkedin.d.mts +145 -0
  52. package/dist/providers/linkedin.d.mts.map +1 -0
  53. package/dist/providers/linkedin.mjs +574 -0
  54. package/dist/providers/linkedin.mjs.map +1 -0
  55. package/dist/providers/reddit.d.mts +102 -0
  56. package/dist/providers/reddit.d.mts.map +1 -0
  57. package/dist/providers/reddit.mjs +657 -0
  58. package/dist/providers/reddit.mjs.map +1 -0
  59. package/dist/providers/telegram.d.mts +139 -0
  60. package/dist/providers/telegram.d.mts.map +1 -0
  61. package/dist/providers/telegram.mjs +517 -0
  62. package/dist/providers/telegram.mjs.map +1 -0
  63. package/dist/providers/tiktok.d.mts +116 -0
  64. package/dist/providers/tiktok.d.mts.map +1 -0
  65. package/dist/providers/tiktok.mjs +676 -0
  66. package/dist/providers/tiktok.mjs.map +1 -0
  67. package/dist/providers/twitter.d.mts +150 -0
  68. package/dist/providers/twitter.d.mts.map +1 -0
  69. package/dist/providers/twitter.mjs +628 -0
  70. package/dist/providers/twitter.mjs.map +1 -0
  71. package/dist/providers/whatsapp.d.mts +79 -0
  72. package/dist/providers/whatsapp.d.mts.map +1 -0
  73. package/dist/providers/whatsapp.mjs +376 -0
  74. package/dist/providers/whatsapp.mjs.map +1 -0
  75. package/dist/providers/youtube.d.mts +153 -0
  76. package/dist/providers/youtube.d.mts.map +1 -0
  77. package/dist/providers/youtube.mjs +902 -0
  78. package/dist/providers/youtube.mjs.map +1 -0
  79. package/dist/reddit-B10kS4Se.mjs +126 -0
  80. package/dist/reddit-B10kS4Se.mjs.map +1 -0
  81. package/dist/schemas/index.d.mts +819 -0
  82. package/dist/schemas/index.d.mts.map +1 -0
  83. package/dist/schemas/index.mjs +31 -0
  84. package/dist/schemas/index.mjs.map +1 -0
  85. package/dist/security-BXhfebWm.d.mts +338 -0
  86. package/dist/security-BXhfebWm.d.mts.map +1 -0
  87. package/dist/shared-Fvc6xQku.mjs +100 -0
  88. package/dist/shared-Fvc6xQku.mjs.map +1 -0
  89. package/dist/telegram-FaUHpZgB.mjs +107 -0
  90. package/dist/telegram-FaUHpZgB.mjs.map +1 -0
  91. package/dist/tiktok-B_bMk4G-.mjs +94 -0
  92. package/dist/tiktok-B_bMk4G-.mjs.map +1 -0
  93. package/dist/twitter-BC22zfuc.mjs +98 -0
  94. package/dist/twitter-BC22zfuc.mjs.map +1 -0
  95. package/dist/types-BFE4psYI.d.mts +102 -0
  96. package/dist/types-BFE4psYI.d.mts.map +1 -0
  97. package/dist/types-Bv27tcT0.d.mts +230 -0
  98. package/dist/types-Bv27tcT0.d.mts.map +1 -0
  99. package/dist/types-BwkKyqpi.d.mts +253 -0
  100. package/dist/types-BwkKyqpi.d.mts.map +1 -0
  101. package/dist/types-CJrHMDV9.mjs +27 -0
  102. package/dist/types-CJrHMDV9.mjs.map +1 -0
  103. package/dist/types-ClbVc2rc.d.mts +117 -0
  104. package/dist/types-ClbVc2rc.d.mts.map +1 -0
  105. package/dist/types-D91N16Ym.d.mts +242 -0
  106. package/dist/types-D91N16Ym.d.mts.map +1 -0
  107. package/dist/types-DfLp_ibQ.d.mts +178 -0
  108. package/dist/types-DfLp_ibQ.d.mts.map +1 -0
  109. package/dist/types-DfjDgEoJ.d.mts +88 -0
  110. package/dist/types-DfjDgEoJ.d.mts.map +1 -0
  111. package/dist/types-Dp5Z9VBr.mjs +23 -0
  112. package/dist/types-Dp5Z9VBr.mjs.map +1 -0
  113. package/dist/types-hriBJTsU.d.mts +129 -0
  114. package/dist/types-hriBJTsU.d.mts.map +1 -0
  115. package/dist/types-rn6UuLL8.d.mts +184 -0
  116. package/dist/types-rn6UuLL8.d.mts.map +1 -0
  117. package/dist/whatsapp-CFp7ryR4.mjs +101 -0
  118. package/dist/whatsapp-CFp7ryR4.mjs.map +1 -0
  119. package/dist/youtube-Bs0fdY7H.mjs +98 -0
  120. package/dist/youtube-Bs0fdY7H.mjs.map +1 -0
  121. package/package.json +148 -0
@@ -0,0 +1,574 @@
1
+ import { t as PlatformProvider } from "../base-Bw7e52V8.mjs";
2
+ import { t as SocialError } from "../errors-Cm6LeKf7.mjs";
3
+ import { t as httpRequest } from "../http-DpcLSR1M.mjs";
4
+ import { t as LinkedInCredentialsSchema } from "../linkedin-70whtVKa.mjs";
5
+
6
+ //#region src/providers/linkedin/index.ts
7
+ /**
8
+ * LinkedIn Platform Provider
9
+ * ==========================
10
+ * LinkedIn integration with OAuth2 and content publishing.
11
+ *
12
+ * Features:
13
+ * - OAuth2 authorization flow (OpenID Connect)
14
+ * - Text post publishing
15
+ * - Article sharing (link posts)
16
+ * - Image post publishing (via pre-registered upload)
17
+ * - Video post publishing (via pre-registered upload)
18
+ * - Multi-image posts
19
+ * - Company page posting (via organization URN)
20
+ * - Post deletion
21
+ * - Credential validation
22
+ *
23
+ * LinkedIn API Quirks:
24
+ * - Uses URN identifiers (urn:li:person:xxx, urn:li:organization:xxx)
25
+ * - Image/video uploads require a 2-step flow: register upload → PUT binary → create post
26
+ * - Token lifespan: access_token ~60 days, refresh_token ~365 days
27
+ * - Rate limits: 100 API calls/day for most endpoints per member
28
+ * - Community Management API requires separate app review
29
+ *
30
+ * @see https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api
31
+ * @see https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow
32
+ */
33
+ const AUTH_URL = "https://www.linkedin.com/oauth/v2/authorization";
34
+ const TOKEN_URL = "https://www.linkedin.com/oauth/v2/accessToken";
35
+ const API_BASE = "https://api.linkedin.com/v2";
36
+ const REST_API_BASE = "https://api.linkedin.com/rest";
37
+ /**
38
+ * Escape special characters for LinkedIn's "little text" format.
39
+ * Without this, characters like ( ) * [ ] { } < > @ | ~ _ may be
40
+ * interpreted as formatting markers by LinkedIn's text parser.
41
+ * @see n8n LinkedIn node — same escaping logic
42
+ */
43
+ function escapeLinkedInText(text) {
44
+ if (!text) return text;
45
+ return text.replace(/[(*)\[\]{}<>@|~_]/g, (char) => "\\" + char);
46
+ }
47
+ var LinkedInProvider = class extends PlatformProvider {
48
+ defaultRedirectUri;
49
+ scopes;
50
+ constructor(cfg = {}) {
51
+ super(cfg);
52
+ this.name = "linkedin";
53
+ this.displayName = "LinkedIn";
54
+ this.authType = "oauth2";
55
+ this.defaultRedirectUri = cfg.redirectUri || `http://localhost:${cfg.port || 8060}/api/oauth/linkedin/callback`;
56
+ this.scopes = [
57
+ "openid",
58
+ "profile",
59
+ "email",
60
+ "w_member_social",
61
+ "w_organization_social"
62
+ ];
63
+ }
64
+ /**
65
+ * Make an authenticated request to LinkedIn API
66
+ * @private
67
+ * @param method - HTTP method
68
+ * @param url - Full URL
69
+ * @param accessToken - OAuth access token
70
+ * @param body - Request body
71
+ * @param extraHeaders - Additional headers
72
+ * @returns Response data
73
+ */
74
+ async _api(method, url, accessToken, body = null, extraHeaders = {}) {
75
+ const isBinary = body instanceof Buffer;
76
+ const result = await httpRequest("linkedin", {
77
+ method: method.toUpperCase(),
78
+ url,
79
+ bearer: accessToken,
80
+ headers: {
81
+ "LinkedIn-Version": "202604",
82
+ "X-Restli-Protocol-Version": "2.0.0",
83
+ ...extraHeaders
84
+ },
85
+ json: !isBinary && body ? body : void 0,
86
+ binary: isBinary ? new Uint8Array(body) : void 0,
87
+ timeout: 6e4,
88
+ retry: { attempts: 2 },
89
+ parseError: (raw, status) => {
90
+ if (raw && typeof raw === "object") {
91
+ const r = raw;
92
+ return {
93
+ message: r.message || r.error_description || `LinkedIn API error (${status})`,
94
+ errorCode: r.serviceErrorCode ?? r.status ?? null
95
+ };
96
+ }
97
+ return null;
98
+ }
99
+ });
100
+ const data = result.data;
101
+ const restliId = result.headers.get("x-restli-id");
102
+ if (restliId && data && typeof data === "object") data["x-restli-id"] = restliId;
103
+ return data;
104
+ }
105
+ /**
106
+ * Get LinkedIn OAuth authorization URL
107
+ */
108
+ getAuthUrl(state, credentials = {}, _options) {
109
+ return `${AUTH_URL}?${new URLSearchParams({
110
+ response_type: "code",
111
+ client_id: credentials.clientId,
112
+ redirect_uri: credentials.redirectUri || this.defaultRedirectUri,
113
+ scope: this.scopes.join(" "),
114
+ state
115
+ }).toString()}`;
116
+ }
117
+ /**
118
+ * Exchange authorization code for tokens
119
+ */
120
+ async exchangeCode(code, credentials = {}) {
121
+ const body = new URLSearchParams({
122
+ grant_type: "authorization_code",
123
+ code,
124
+ client_id: credentials.clientId,
125
+ client_secret: credentials.clientSecret,
126
+ redirect_uri: credentials.redirectUri || this.defaultRedirectUri
127
+ });
128
+ const data = await (await fetch(TOKEN_URL, {
129
+ method: "POST",
130
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
131
+ body: body.toString()
132
+ })).json();
133
+ if (data.error) throw new SocialError("linkedin", data.error_description || data.error);
134
+ return {
135
+ access_token: data.access_token,
136
+ refresh_token: data.refresh_token || null,
137
+ expires_in: data.expires_in,
138
+ refresh_token_expires_in: data.refresh_token_expires_in || null,
139
+ scope: data.scope,
140
+ token_type: data.token_type || "Bearer"
141
+ };
142
+ }
143
+ /**
144
+ * Refresh access token
145
+ */
146
+ async refreshToken(refreshToken, credentials = {}) {
147
+ const body = new URLSearchParams({
148
+ grant_type: "refresh_token",
149
+ refresh_token: refreshToken,
150
+ client_id: credentials.clientId,
151
+ client_secret: credentials.clientSecret
152
+ });
153
+ const data = await (await fetch(TOKEN_URL, {
154
+ method: "POST",
155
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
156
+ body: body.toString()
157
+ })).json();
158
+ if (data.error) throw new SocialError("linkedin", data.error_description || "Token refresh failed");
159
+ return {
160
+ access_token: data.access_token,
161
+ refresh_token: data.refresh_token || refreshToken,
162
+ expires_in: data.expires_in,
163
+ refresh_token_expires_in: data.refresh_token_expires_in || null,
164
+ token_type: data.token_type || "Bearer"
165
+ };
166
+ }
167
+ /**
168
+ * Get authenticated user's profile info
169
+ */
170
+ async getAccountInfo(accessToken) {
171
+ const profile = await this._api("GET", `${API_BASE}/userinfo`, accessToken);
172
+ return {
173
+ id: profile.sub,
174
+ name: profile.name,
175
+ email: profile.email,
176
+ profileImage: profile.picture
177
+ };
178
+ }
179
+ /**
180
+ * Test credential validity
181
+ */
182
+ async testCredential(credentialData) {
183
+ try {
184
+ if (credentialData.oauthTokenData) {
185
+ const tokenData = typeof credentialData.oauthTokenData === "string" ? JSON.parse(credentialData.oauthTokenData) : credentialData.oauthTokenData;
186
+ const accountInfo = await this.getAccountInfo(tokenData.access_token);
187
+ return {
188
+ status: "OK",
189
+ message: `Connected as ${accountInfo.name}`,
190
+ data: {
191
+ channelId: accountInfo.id,
192
+ channelTitle: accountInfo.name,
193
+ profileImage: accountInfo.profileImage
194
+ }
195
+ };
196
+ }
197
+ if (!credentialData.clientId || !credentialData.clientSecret) return {
198
+ status: "Error",
199
+ message: "LinkedIn Client ID and Client Secret are required"
200
+ };
201
+ return {
202
+ status: "Pending",
203
+ message: "Credential needs OAuth authorization. Click \"Connect Account\" to link your LinkedIn."
204
+ };
205
+ } catch (error) {
206
+ return {
207
+ status: "Error",
208
+ message: error.message || "Failed to validate LinkedIn credential"
209
+ };
210
+ }
211
+ }
212
+ /**
213
+ * Create a text post on LinkedIn
214
+ * @param accessToken
215
+ * @param authorUrn - Person URN (urn:li:person:xxx) or Organization URN (urn:li:organization:xxx)
216
+ * @param text - Post text (up to 3000 characters)
217
+ * @param options
218
+ */
219
+ async createTextPost(accessToken, authorUrn, text, options = {}) {
220
+ const visibility = options.visibility || "PUBLIC";
221
+ return this._api("POST", `${REST_API_BASE}/posts`, accessToken, {
222
+ author: authorUrn,
223
+ commentary: escapeLinkedInText(text).substring(0, 3e3),
224
+ visibility,
225
+ distribution: {
226
+ feedDistribution: "MAIN_FEED",
227
+ targetEntities: [],
228
+ thirdPartyDistributionChannels: []
229
+ },
230
+ lifecycleState: "PUBLISHED"
231
+ });
232
+ }
233
+ /**
234
+ * Create a link/article share post
235
+ * @param accessToken
236
+ * @param authorUrn
237
+ * @param text - Commentary text
238
+ * @param articleUrl - URL to share
239
+ * @param options
240
+ */
241
+ async createArticlePost(accessToken, authorUrn, text, articleUrl, options = {}) {
242
+ const content = { article: { source: articleUrl } };
243
+ if (options.title) content.article.title = options.title;
244
+ if (options.description) content.article.description = options.description;
245
+ return this._api("POST", `${REST_API_BASE}/posts`, accessToken, {
246
+ author: authorUrn,
247
+ commentary: escapeLinkedInText(text).substring(0, 3e3),
248
+ visibility: options.visibility || "PUBLIC",
249
+ distribution: {
250
+ feedDistribution: "MAIN_FEED",
251
+ targetEntities: [],
252
+ thirdPartyDistributionChannels: []
253
+ },
254
+ content,
255
+ lifecycleState: "PUBLISHED"
256
+ });
257
+ }
258
+ /**
259
+ * Register an image upload and get the upload URL
260
+ * @private
261
+ * @param accessToken
262
+ * @param ownerUrn - Person or Organization URN
263
+ */
264
+ async _registerImageUpload(accessToken, ownerUrn) {
265
+ const data = await this._api("POST", `${REST_API_BASE}/images?action=initializeUpload`, accessToken, { initializeUploadRequest: { owner: ownerUrn } });
266
+ return {
267
+ uploadUrl: data.value.uploadUrl,
268
+ imageUrn: data.value.image
269
+ };
270
+ }
271
+ /**
272
+ * Upload an image binary to LinkedIn's upload URL
273
+ * @private
274
+ * @param uploadUrl - Upload URL from registerImageUpload
275
+ * @param accessToken
276
+ * @param imageBuffer - Image binary data
277
+ */
278
+ async _uploadImageBinary(uploadUrl, accessToken, imageBuffer) {
279
+ await this._api("PUT", uploadUrl, accessToken, imageBuffer, { "Content-Type": "application/octet-stream" });
280
+ }
281
+ /**
282
+ * Create an image post on LinkedIn
283
+ * @param accessToken
284
+ * @param authorUrn
285
+ * @param text - Post commentary
286
+ * @param imageBuffer - Image binary data
287
+ * @param options
288
+ */
289
+ async createImagePost(accessToken, authorUrn, text, imageBuffer, options = {}) {
290
+ const { uploadUrl, imageUrn } = await this._registerImageUpload(accessToken, authorUrn);
291
+ await this._uploadImageBinary(uploadUrl, accessToken, imageBuffer);
292
+ return this._api("POST", `${REST_API_BASE}/posts`, accessToken, {
293
+ author: authorUrn,
294
+ commentary: escapeLinkedInText(text).substring(0, 3e3),
295
+ visibility: options.visibility || "PUBLIC",
296
+ distribution: {
297
+ feedDistribution: "MAIN_FEED",
298
+ targetEntities: [],
299
+ thirdPartyDistributionChannels: []
300
+ },
301
+ content: { media: {
302
+ id: imageUrn,
303
+ title: options.altText || ""
304
+ } },
305
+ lifecycleState: "PUBLISHED"
306
+ });
307
+ }
308
+ /**
309
+ * Register a video upload and get the upload URL
310
+ * @private
311
+ * @param accessToken
312
+ * @param ownerUrn
313
+ * @param fileSizeBytes
314
+ */
315
+ async _registerVideoUpload(accessToken, ownerUrn, fileSizeBytes) {
316
+ const data = await this._api("POST", `${REST_API_BASE}/videos?action=initializeUpload`, accessToken, { initializeUploadRequest: {
317
+ owner: ownerUrn,
318
+ fileSizeBytes,
319
+ uploadCaptions: false,
320
+ uploadThumbnail: false
321
+ } });
322
+ return {
323
+ uploadUrl: data.value.uploadInstructions[0]?.uploadUrl,
324
+ videoUrn: data.value.video
325
+ };
326
+ }
327
+ /**
328
+ * Upload a video to a LinkedIn post
329
+ * LinkedIn video uploads: register → PUT binary → create post
330
+ *
331
+ * @param params
332
+ */
333
+ async uploadVideo(params) {
334
+ const { videoBuffer, title = "", description = "", tokens, onProgress } = params;
335
+ const accessToken = tokens.access_token;
336
+ const authorUrn = tokens.organizationUrn || `urn:li:person:${tokens.personId}`;
337
+ if (!videoBuffer || !videoBuffer.length) throw new SocialError("linkedin", "Video buffer is required for LinkedIn video upload.");
338
+ if (onProgress) onProgress(5);
339
+ const { uploadUrl, videoUrn } = await this._registerVideoUpload(accessToken, authorUrn, videoBuffer.length);
340
+ if (onProgress) onProgress(10);
341
+ await this._api("PUT", uploadUrl, accessToken, videoBuffer, { "Content-Type": "application/octet-stream" });
342
+ if (onProgress) onProgress(80);
343
+ const commentary = escapeLinkedInText([title, description].filter(Boolean).join("\n\n")).substring(0, 3e3);
344
+ const post = await this._api("POST", `${REST_API_BASE}/posts`, accessToken, {
345
+ author: authorUrn,
346
+ commentary,
347
+ visibility: "PUBLIC",
348
+ distribution: {
349
+ feedDistribution: "MAIN_FEED",
350
+ targetEntities: [],
351
+ thirdPartyDistributionChannels: []
352
+ },
353
+ content: { media: { id: videoUrn } },
354
+ lifecycleState: "PUBLISHED"
355
+ });
356
+ if (onProgress) onProgress(100);
357
+ const postUrn = post?.["x-restli-id"] || post?.id || videoUrn;
358
+ return {
359
+ platformVideoId: postUrn,
360
+ platformUrl: `https://www.linkedin.com/feed/update/${encodeURIComponent(postUrn)}`,
361
+ status: "published",
362
+ uploadedAt: /* @__PURE__ */ new Date(),
363
+ metadata: {
364
+ authorUrn,
365
+ videoUrn,
366
+ commentary
367
+ }
368
+ };
369
+ }
370
+ /**
371
+ * Delete a post
372
+ * @param accessToken
373
+ * @param postUrn - Post URN (urn:li:share:xxx or urn:li:ugcPost:xxx)
374
+ */
375
+ async deletePost(accessToken, postUrn) {
376
+ return this._api("DELETE", `${REST_API_BASE}/posts/${encodeURIComponent(postUrn)}`, accessToken);
377
+ }
378
+ /**
379
+ * Get a single post by its URN
380
+ * @param accessToken - OAuth access token
381
+ * @param postUrn - Post URN (e.g. urn:li:share:xxx or urn:li:ugcPost:xxx)
382
+ * @returns Parsed post data
383
+ * @see https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api#get-posts-by-id
384
+ */
385
+ async getPost(accessToken, postUrn) {
386
+ if (!postUrn || !postUrn.startsWith("urn:li:")) throw new SocialError("linkedin", `Invalid post URN: "${postUrn}". Expected format: urn:li:share:xxx or urn:li:ugcPost:xxx`, {
387
+ statusCode: 400,
388
+ errorCode: "INVALID_URN"
389
+ });
390
+ const encodedUrn = encodeURIComponent(postUrn);
391
+ const data = await this._api("GET", `${REST_API_BASE}/posts/${encodedUrn}`, accessToken);
392
+ return {
393
+ id: data.id || postUrn,
394
+ authorUrn: data.author || "",
395
+ text: data.commentary || "",
396
+ createdAt: data.createdAt ? new Date(data.createdAt).toISOString() : "",
397
+ visibility: data.visibility || "",
398
+ lifecycleState: data.lifecycleState || "",
399
+ commentary: data.commentary,
400
+ shareUrl: data.id ? `https://www.linkedin.com/feed/update/${encodeURIComponent(data.id)}` : null
401
+ };
402
+ }
403
+ /**
404
+ * Get analytics/social metadata for a post
405
+ *
406
+ * Uses the socialMetadata endpoint which returns reaction counts and comment counts.
407
+ * Note: Detailed impression/click analytics require the Marketing API (r_organization_social
408
+ * scope + ads access). This method returns what's available via the community management API.
409
+ *
410
+ * @param accessToken - OAuth access token
411
+ * @param postUrn - Post URN
412
+ * @returns Parsed analytics data (some fields may be null if unavailable)
413
+ */
414
+ async getPostAnalytics(accessToken, postUrn) {
415
+ if (!postUrn || !postUrn.startsWith("urn:li:")) throw new SocialError("linkedin", `Invalid post URN: "${postUrn}". Expected format: urn:li:share:xxx or urn:li:ugcPost:xxx`, {
416
+ statusCode: 400,
417
+ errorCode: "INVALID_URN"
418
+ });
419
+ const encodedUrn = encodeURIComponent(postUrn);
420
+ const data = await this._api("GET", `${REST_API_BASE}/socialMetadata/${encodedUrn}`, accessToken);
421
+ const reactionSummaries = data?.reactionSummaries || [];
422
+ const totalReactions = reactionSummaries.reduce((sum, r) => sum + (r.count || 0), 0);
423
+ const likeCount = reactionSummaries.find((r) => r.reactionType === "LIKE")?.count ?? null;
424
+ const commentCount = data?.commentSummary?.totalFirstLevelComments ?? data?.totalComments ?? null;
425
+ const shareCount = data?.totalShares ?? null;
426
+ return {
427
+ impressionCount: null,
428
+ clickCount: null,
429
+ likeCount: likeCount !== null ? likeCount : totalReactions > 0 ? totalReactions : null,
430
+ commentCount,
431
+ shareCount,
432
+ engagementRate: null
433
+ };
434
+ }
435
+ /**
436
+ * Get organizations the authenticated user administers.
437
+ * Used to select a company page for posting as an organization.
438
+ */
439
+ async getOrganizations(accessToken) {
440
+ try {
441
+ return ((await this._api("GET", `${REST_API_BASE}/organizationAcls?q=roleAssignee&role=ADMINISTRATOR&projection=(elements*(organization~(id,localizedName,vanityName,logoV2(original~:playableStreams))))`, accessToken))?.elements || []).map((el) => {
442
+ const org = el["organization~"] || {};
443
+ const logoUrl = (org.logoV2?.["original~"]?.elements)?.[0]?.identifiers?.[0]?.identifier;
444
+ return {
445
+ id: org.id ? String(org.id) : "",
446
+ name: org.localizedName || "",
447
+ vanityName: org.vanityName || "",
448
+ logoUrl: logoUrl || void 0
449
+ };
450
+ });
451
+ } catch (error) {
452
+ if (error instanceof SocialError) throw error;
453
+ throw new SocialError("linkedin", `Failed to get organizations: ${error.message}`, { originalError: error });
454
+ }
455
+ }
456
+ /**
457
+ * Create a post with multiple images.
458
+ * Registers N image uploads, uploads N binaries, then creates a multi-image post.
459
+ *
460
+ * @param accessToken - OAuth access token
461
+ * @param authorUrn - Person or Organization URN
462
+ * @param text - Post commentary text
463
+ * @param imageBuffers - Array of image binaries
464
+ * @param options - Optional alt texts and visibility
465
+ */
466
+ async createMultiImagePost(accessToken, authorUrn, text, imageBuffers, options = {}) {
467
+ if (!imageBuffers.length) throw new SocialError("linkedin", "At least one image is required for multi-image post", { statusCode: 400 });
468
+ try {
469
+ const uploadRegistrations = await Promise.all(imageBuffers.map(() => this._registerImageUpload(accessToken, authorUrn)));
470
+ await Promise.all(imageBuffers.map((buffer, i) => this._uploadImageBinary(uploadRegistrations[i].uploadUrl, accessToken, buffer)));
471
+ const images = uploadRegistrations.map((reg, i) => ({
472
+ id: reg.imageUrn,
473
+ altText: options.altTexts?.[i] || ""
474
+ }));
475
+ const post = await this._api("POST", `${REST_API_BASE}/posts`, accessToken, {
476
+ author: authorUrn,
477
+ commentary: escapeLinkedInText(text).substring(0, 3e3),
478
+ visibility: options.visibility || "PUBLIC",
479
+ distribution: {
480
+ feedDistribution: "MAIN_FEED",
481
+ targetEntities: [],
482
+ thirdPartyDistributionChannels: []
483
+ },
484
+ content: { multiImage: { images } },
485
+ lifecycleState: "PUBLISHED"
486
+ });
487
+ const postUrn = post?.["x-restli-id"] || post?.id || "";
488
+ return {
489
+ platformPostId: postUrn,
490
+ platformUrl: postUrn ? `https://www.linkedin.com/feed/update/${encodeURIComponent(postUrn)}` : null,
491
+ status: "published",
492
+ uploadedAt: /* @__PURE__ */ new Date(),
493
+ metadata: {
494
+ authorUrn,
495
+ imageCount: imageBuffers.length
496
+ }
497
+ };
498
+ } catch (error) {
499
+ if (error instanceof SocialError) throw error;
500
+ throw new SocialError("linkedin", `Failed to create multi-image post: ${error.message}`, { originalError: error });
501
+ }
502
+ }
503
+ getCredentialZodSchema() {
504
+ return LinkedInCredentialsSchema;
505
+ }
506
+ getCredentialSchema() {
507
+ return [{
508
+ name: "clientId",
509
+ displayName: "Client ID",
510
+ type: "text",
511
+ required: true,
512
+ description: "OAuth2 Client ID from LinkedIn Developer Portal"
513
+ }, {
514
+ name: "clientSecret",
515
+ displayName: "Client Secret",
516
+ type: "password",
517
+ required: true,
518
+ description: "OAuth2 Client Secret from LinkedIn Developer Portal"
519
+ }];
520
+ }
521
+ getMetadata() {
522
+ return {
523
+ name: this.name,
524
+ displayName: this.displayName,
525
+ authType: this.authType,
526
+ icon: "linkedin",
527
+ brandColor: "#0A66C2",
528
+ description: "Publish posts, articles, and videos on LinkedIn",
529
+ scopes: this.scopes,
530
+ scopeDescriptions: {
531
+ openid: "Sign in with LinkedIn (OpenID Connect)",
532
+ profile: "View your basic profile info (name, photo)",
533
+ email: "View your email address",
534
+ w_member_social: "Create, modify, and delete posts on your behalf",
535
+ w_organization_social: "Post on behalf of organizations/company pages you manage"
536
+ },
537
+ supportsScheduling: false,
538
+ supportsEnvironment: false,
539
+ setupGuide: [
540
+ {
541
+ step: 1,
542
+ title: "Create LinkedIn App",
543
+ description: "Go to linkedin.com/developers → \"Create App\". Fill in app name, LinkedIn Page, and logo"
544
+ },
545
+ {
546
+ step: 2,
547
+ title: "Request Products",
548
+ description: "In your app's \"Products\" tab, request access to \"Share on LinkedIn\" and \"Sign In with LinkedIn using OpenID Connect\""
549
+ },
550
+ {
551
+ step: 3,
552
+ title: "Configure OAuth",
553
+ description: "In the \"Auth\" tab, add the redirect URL shown below to \"Authorized redirect URLs for your app\""
554
+ },
555
+ {
556
+ step: 4,
557
+ title: "Copy Credentials",
558
+ description: "Copy \"Client ID\" and \"Client Secret\" from the \"Auth\" tab and paste them above"
559
+ },
560
+ {
561
+ step: 5,
562
+ title: "Verify Scopes",
563
+ description: "Ensure your app has these OAuth 2.0 scopes: openid, profile, email, w_member_social, w_organization_social"
564
+ }
565
+ ],
566
+ redirectUriPattern: this.defaultRedirectUri,
567
+ credentialSchema: this.getCredentialSchema()
568
+ };
569
+ }
570
+ };
571
+
572
+ //#endregion
573
+ export { LinkedInProvider };
574
+ //# sourceMappingURL=linkedin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linkedin.mjs","names":[],"sources":["../../src/providers/linkedin/index.ts"],"sourcesContent":["/**\n * LinkedIn Platform Provider\n * ==========================\n * LinkedIn integration with OAuth2 and content publishing.\n *\n * Features:\n * - OAuth2 authorization flow (OpenID Connect)\n * - Text post publishing\n * - Article sharing (link posts)\n * - Image post publishing (via pre-registered upload)\n * - Video post publishing (via pre-registered upload)\n * - Multi-image posts\n * - Company page posting (via organization URN)\n * - Post deletion\n * - Credential validation\n *\n * LinkedIn API Quirks:\n * - Uses URN identifiers (urn:li:person:xxx, urn:li:organization:xxx)\n * - Image/video uploads require a 2-step flow: register upload → PUT binary → create post\n * - Token lifespan: access_token ~60 days, refresh_token ~365 days\n * - Rate limits: 100 API calls/day for most endpoints per member\n * - Community Management API requires separate app review\n *\n * @see https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api\n * @see https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow\n */\n\nimport {\n PlatformProvider,\n type AuthUrlOptions,\n type CredentialField,\n type ProviderMetadata,\n type UploadParams,\n type UploadResult,\n type TestResult,\n type AccountInfo,\n type OAuthTokens,\n type ProviderConfig,\n} from '../../base.js';\nimport { SocialError } from '../../errors.js';\nimport { LinkedInCredentialsSchema } from '../../schemas/linkedin.js';\nimport { httpRequest } from '../../common/http.js';\nimport type { z } from 'zod';\nimport type {\n LinkedInCredentials,\n LinkedInTokenData,\n LinkedInCredentialData,\n TextPostOptions,\n ArticlePostOptions,\n ImagePostOptions,\n MultiImagePostOptions,\n ImageUploadResult,\n VideoUploadResult,\n LinkedInUploadParams,\n LinkedInSetupStep,\n LinkedInPost,\n LinkedInPostAnalytics,\n LinkedInOrganization,\n} from './types.js';\n\n// Re-export all types\nexport type {\n LinkedInCredentials,\n LinkedInTokenData,\n LinkedInCredentialData,\n TextPostOptions,\n ArticlePostOptions,\n ImagePostOptions,\n MultiImagePostOptions,\n ImageUploadResult,\n VideoUploadResult,\n LinkedInUploadParams,\n LinkedInSetupStep,\n LinkedInPost,\n LinkedInPostAnalytics,\n LinkedInOrganization,\n} from './types.js';\n\n// ─── API Endpoints ────────────────────────────────────────────────────────────\n\nconst AUTH_URL = 'https://www.linkedin.com/oauth/v2/authorization';\nconst TOKEN_URL = 'https://www.linkedin.com/oauth/v2/accessToken';\nconst API_BASE = 'https://api.linkedin.com/v2';\nconst REST_API_BASE = 'https://api.linkedin.com/rest';\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\n/**\n * Escape special characters for LinkedIn's \"little text\" format.\n * Without this, characters like ( ) * [ ] { } < > @ | ~ _ may be\n * interpreted as formatting markers by LinkedIn's text parser.\n * @see n8n LinkedIn node — same escaping logic\n */\nfunction escapeLinkedInText(text: string): string {\n if (!text) return text;\n return text.replace(/[(*)\\[\\]{}<>@|~_]/g, (char: string) => '\\\\' + char);\n}\n\n// ─── Provider ─────────────────────────────────────────────────────────────────\n\nexport class LinkedInProvider extends PlatformProvider {\n public defaultRedirectUri: string;\n public scopes: string[];\n\n constructor(cfg: ProviderConfig = {}) {\n super(cfg);\n this.name = 'linkedin';\n this.displayName = 'LinkedIn';\n this.authType = 'oauth2';\n\n this.defaultRedirectUri =\n cfg.redirectUri ||\n `http://localhost:${cfg.port || 8060}/api/oauth/linkedin/callback`;\n\n this.scopes = [\n 'openid',\n 'profile',\n 'email',\n 'w_member_social',\n 'w_organization_social',\n ];\n }\n\n // ─── API Helper ──────────────────────────────────────────────────────\n\n /**\n * Make an authenticated request to LinkedIn API\n * @private\n * @param method - HTTP method\n * @param url - Full URL\n * @param accessToken - OAuth access token\n * @param body - Request body\n * @param extraHeaders - Additional headers\n * @returns Response data\n */\n async _api(\n method: string,\n url: string,\n accessToken: string,\n body: Record<string, any> | Buffer | null = null,\n extraHeaders: Record<string, string> = {},\n ): Promise<any> {\n const isBinary = body instanceof Buffer;\n const result = await httpRequest<any>('linkedin', {\n method: method.toUpperCase() as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n url,\n bearer: accessToken,\n headers: {\n 'LinkedIn-Version': '202604',\n 'X-Restli-Protocol-Version': '2.0.0',\n ...extraHeaders,\n },\n json: !isBinary && body ? body : undefined,\n binary: isBinary ? new Uint8Array(body as Buffer) : undefined,\n timeout: 60_000,\n retry: { attempts: 2 },\n parseError: (raw, status) => {\n if (raw && typeof raw === 'object') {\n const r = raw as Record<string, unknown>;\n return {\n message: (r.message as string) || (r.error_description as string) || `LinkedIn API error (${status})`,\n errorCode: (r.serviceErrorCode as number | string | null) ?? (r.status as number | null) ?? null,\n };\n }\n return null;\n },\n });\n\n // On 201 Created, LinkedIn returns the resource URN in the x-restli-id header.\n const data = result.data;\n const restliId = result.headers.get('x-restli-id');\n if (restliId && data && typeof data === 'object') {\n (data as Record<string, unknown>)['x-restli-id'] = restliId;\n }\n return data;\n }\n\n // ─── OAuth ───────────────────────────────────────────────────────────\n\n /**\n * Get LinkedIn OAuth authorization URL\n */\n getAuthUrl(state: string, credentials: LinkedInCredentials | Record<string, any> = {}, _options?: AuthUrlOptions): string {\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: credentials.clientId,\n redirect_uri: credentials.redirectUri || this.defaultRedirectUri,\n scope: this.scopes.join(' '),\n state,\n });\n\n return `${AUTH_URL}?${params.toString()}`;\n }\n\n /**\n * Exchange authorization code for tokens\n */\n async exchangeCode(code: string, credentials: LinkedInCredentials | Record<string, any> = {}): Promise<OAuthTokens> {\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n redirect_uri: credentials.redirectUri || this.defaultRedirectUri,\n });\n\n const response = await fetch(TOKEN_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n\n const data: any = await response.json();\n\n if (data.error) {\n throw new SocialError('linkedin', data.error_description || data.error);\n }\n\n return {\n access_token: data.access_token,\n refresh_token: data.refresh_token || null,\n expires_in: data.expires_in,\n refresh_token_expires_in: data.refresh_token_expires_in || null,\n scope: data.scope,\n token_type: data.token_type || 'Bearer',\n };\n }\n\n /**\n * Refresh access token\n */\n async refreshToken(refreshToken: string, credentials: LinkedInCredentials | Record<string, any> = {}): Promise<OAuthTokens> {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n });\n\n const response = await fetch(TOKEN_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n\n const data: any = await response.json();\n\n if (data.error) {\n throw new SocialError('linkedin', data.error_description || 'Token refresh failed');\n }\n\n return {\n access_token: data.access_token,\n refresh_token: data.refresh_token || refreshToken,\n expires_in: data.expires_in,\n refresh_token_expires_in: data.refresh_token_expires_in || null,\n token_type: data.token_type || 'Bearer',\n };\n }\n\n // ─── Account Info ────────────────────────────────────────────────────\n\n /**\n * Get authenticated user's profile info\n */\n async getAccountInfo(accessToken: string): Promise<AccountInfo> {\n const profile: any = await this._api('GET', `${API_BASE}/userinfo`, accessToken);\n\n return {\n id: profile.sub,\n name: profile.name,\n email: profile.email,\n profileImage: profile.picture,\n };\n }\n\n /**\n * Test credential validity\n */\n async testCredential(credentialData: LinkedInCredentialData): Promise<TestResult> {\n try {\n if (credentialData.oauthTokenData) {\n const tokenData: LinkedInTokenData = typeof credentialData.oauthTokenData === 'string'\n ? JSON.parse(credentialData.oauthTokenData)\n : credentialData.oauthTokenData;\n const accountInfo = await this.getAccountInfo(tokenData.access_token);\n\n return {\n status: 'OK',\n message: `Connected as ${accountInfo.name}`,\n data: {\n channelId: accountInfo.id,\n channelTitle: accountInfo.name,\n profileImage: accountInfo.profileImage,\n },\n } as TestResult;\n }\n\n if (!credentialData.clientId || !credentialData.clientSecret) {\n return {\n status: 'Error',\n message: 'LinkedIn Client ID and Client Secret are required',\n };\n }\n\n return {\n status: 'Pending' as TestResult['status'],\n message: 'Credential needs OAuth authorization. Click \"Connect Account\" to link your LinkedIn.',\n };\n } catch (error: any) {\n return {\n status: 'Error',\n message: error.message || 'Failed to validate LinkedIn credential',\n };\n }\n }\n\n // ─── Content Publishing ──────────────────────────────────────────────\n\n /**\n * Create a text post on LinkedIn\n * @param accessToken\n * @param authorUrn - Person URN (urn:li:person:xxx) or Organization URN (urn:li:organization:xxx)\n * @param text - Post text (up to 3000 characters)\n * @param options\n */\n async createTextPost(\n accessToken: string,\n authorUrn: string,\n text: string,\n options: TextPostOptions = {},\n ): Promise<any> {\n const visibility = options.visibility || 'PUBLIC';\n\n return this._api('POST', `${REST_API_BASE}/posts`, accessToken, {\n author: authorUrn,\n commentary: escapeLinkedInText(text).substring(0, 3000),\n visibility,\n distribution: {\n feedDistribution: 'MAIN_FEED',\n targetEntities: [],\n thirdPartyDistributionChannels: [],\n },\n lifecycleState: 'PUBLISHED',\n });\n }\n\n /**\n * Create a link/article share post\n * @param accessToken\n * @param authorUrn\n * @param text - Commentary text\n * @param articleUrl - URL to share\n * @param options\n */\n async createArticlePost(\n accessToken: string,\n authorUrn: string,\n text: string,\n articleUrl: string,\n options: ArticlePostOptions = {},\n ): Promise<any> {\n const content: Record<string, any> = {\n article: {\n source: articleUrl,\n },\n };\n\n if (options.title) content.article.title = options.title;\n if (options.description) content.article.description = options.description;\n\n return this._api('POST', `${REST_API_BASE}/posts`, accessToken, {\n author: authorUrn,\n commentary: escapeLinkedInText(text).substring(0, 3000),\n visibility: options.visibility || 'PUBLIC',\n distribution: {\n feedDistribution: 'MAIN_FEED',\n targetEntities: [],\n thirdPartyDistributionChannels: [],\n },\n content,\n lifecycleState: 'PUBLISHED',\n });\n }\n\n /**\n * Register an image upload and get the upload URL\n * @private\n * @param accessToken\n * @param ownerUrn - Person or Organization URN\n */\n async _registerImageUpload(accessToken: string, ownerUrn: string): Promise<ImageUploadResult> {\n const data: any = await this._api('POST', `${REST_API_BASE}/images?action=initializeUpload`, accessToken, {\n initializeUploadRequest: {\n owner: ownerUrn,\n },\n });\n\n return {\n uploadUrl: data.value.uploadUrl,\n imageUrn: data.value.image,\n };\n }\n\n /**\n * Upload an image binary to LinkedIn's upload URL\n * @private\n * @param uploadUrl - Upload URL from registerImageUpload\n * @param accessToken\n * @param imageBuffer - Image binary data\n */\n async _uploadImageBinary(uploadUrl: string, accessToken: string, imageBuffer: Buffer): Promise<void> {\n await this._api('PUT', uploadUrl, accessToken, imageBuffer, {\n 'Content-Type': 'application/octet-stream',\n });\n }\n\n /**\n * Create an image post on LinkedIn\n * @param accessToken\n * @param authorUrn\n * @param text - Post commentary\n * @param imageBuffer - Image binary data\n * @param options\n */\n async createImagePost(\n accessToken: string,\n authorUrn: string,\n text: string,\n imageBuffer: Buffer,\n options: ImagePostOptions = {},\n ): Promise<any> {\n // Step 1: Register upload\n const { uploadUrl, imageUrn } = await this._registerImageUpload(accessToken, authorUrn);\n\n // Step 2: Upload binary\n await this._uploadImageBinary(uploadUrl, accessToken, imageBuffer);\n\n // Step 3: Create post with image\n return this._api('POST', `${REST_API_BASE}/posts`, accessToken, {\n author: authorUrn,\n commentary: escapeLinkedInText(text).substring(0, 3000),\n visibility: options.visibility || 'PUBLIC',\n distribution: {\n feedDistribution: 'MAIN_FEED',\n targetEntities: [],\n thirdPartyDistributionChannels: [],\n },\n content: {\n media: {\n id: imageUrn,\n title: options.altText || '',\n },\n },\n lifecycleState: 'PUBLISHED',\n });\n }\n\n /**\n * Register a video upload and get the upload URL\n * @private\n * @param accessToken\n * @param ownerUrn\n * @param fileSizeBytes\n */\n async _registerVideoUpload(\n accessToken: string,\n ownerUrn: string,\n fileSizeBytes: number,\n ): Promise<VideoUploadResult> {\n const data: any = await this._api('POST', `${REST_API_BASE}/videos?action=initializeUpload`, accessToken, {\n initializeUploadRequest: {\n owner: ownerUrn,\n fileSizeBytes,\n uploadCaptions: false,\n uploadThumbnail: false,\n },\n });\n\n return {\n uploadUrl: data.value.uploadInstructions[0]?.uploadUrl,\n videoUrn: data.value.video,\n };\n }\n\n /**\n * Upload a video to a LinkedIn post\n * LinkedIn video uploads: register → PUT binary → create post\n *\n * @param params\n */\n async uploadVideo(params: LinkedInUploadParams): Promise<UploadResult> {\n const {\n videoBuffer,\n title = '',\n description = '',\n tokens,\n onProgress,\n } = params;\n\n const accessToken = tokens.access_token;\n const authorUrn = tokens.organizationUrn || `urn:li:person:${tokens.personId}`;\n\n if (!videoBuffer || !videoBuffer.length) {\n throw new SocialError('linkedin', 'Video buffer is required for LinkedIn video upload.');\n }\n\n if (onProgress) onProgress(5);\n\n // Step 1: Register upload\n const { uploadUrl, videoUrn } = await this._registerVideoUpload(\n accessToken,\n authorUrn,\n videoBuffer.length,\n );\n\n if (onProgress) onProgress(10);\n\n // Step 2: Upload binary\n await this._api('PUT', uploadUrl, accessToken, videoBuffer, {\n 'Content-Type': 'application/octet-stream',\n });\n\n if (onProgress) onProgress(80);\n\n // Step 3: Create post with video\n const commentary = escapeLinkedInText([title, description].filter(Boolean).join('\\n\\n')).substring(0, 3000);\n\n const post: any = await this._api('POST', `${REST_API_BASE}/posts`, accessToken, {\n author: authorUrn,\n commentary,\n visibility: 'PUBLIC',\n distribution: {\n feedDistribution: 'MAIN_FEED',\n targetEntities: [],\n thirdPartyDistributionChannels: [],\n },\n content: {\n media: { id: videoUrn },\n },\n lifecycleState: 'PUBLISHED',\n });\n\n if (onProgress) onProgress(100);\n\n // Extract post ID from the response header or URN\n const postUrn = post?.['x-restli-id'] || post?.id || videoUrn;\n\n return {\n platformVideoId: postUrn,\n platformUrl: `https://www.linkedin.com/feed/update/${encodeURIComponent(postUrn)}`,\n status: 'published',\n uploadedAt: new Date(),\n metadata: {\n authorUrn,\n videoUrn,\n commentary,\n },\n };\n }\n\n /**\n * Delete a post\n * @param accessToken\n * @param postUrn - Post URN (urn:li:share:xxx or urn:li:ugcPost:xxx)\n */\n async deletePost(accessToken: string, postUrn: string): Promise<any> {\n return this._api('DELETE', `${REST_API_BASE}/posts/${encodeURIComponent(postUrn)}`, accessToken);\n }\n\n // ─── Post Read ─────────────────────────────────────────────────────────\n\n /**\n * Get a single post by its URN\n * @param accessToken - OAuth access token\n * @param postUrn - Post URN (e.g. urn:li:share:xxx or urn:li:ugcPost:xxx)\n * @returns Parsed post data\n * @see https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api#get-posts-by-id\n */\n async getPost(accessToken: string, postUrn: string): Promise<LinkedInPost> {\n if (!postUrn || !postUrn.startsWith('urn:li:')) {\n throw new SocialError('linkedin', `Invalid post URN: \"${postUrn}\". Expected format: urn:li:share:xxx or urn:li:ugcPost:xxx`, {\n statusCode: 400,\n errorCode: 'INVALID_URN',\n });\n }\n\n const encodedUrn = encodeURIComponent(postUrn);\n const data: any = await this._api('GET', `${REST_API_BASE}/posts/${encodedUrn}`, accessToken);\n\n return {\n id: data.id || postUrn,\n authorUrn: data.author || '',\n text: data.commentary || '',\n createdAt: data.createdAt ? new Date(data.createdAt).toISOString() : '',\n visibility: data.visibility || '',\n lifecycleState: data.lifecycleState || '',\n commentary: data.commentary,\n shareUrl: data.id\n ? `https://www.linkedin.com/feed/update/${encodeURIComponent(data.id)}`\n : null,\n };\n }\n\n /**\n * Get analytics/social metadata for a post\n *\n * Uses the socialMetadata endpoint which returns reaction counts and comment counts.\n * Note: Detailed impression/click analytics require the Marketing API (r_organization_social\n * scope + ads access). This method returns what's available via the community management API.\n *\n * @param accessToken - OAuth access token\n * @param postUrn - Post URN\n * @returns Parsed analytics data (some fields may be null if unavailable)\n */\n async getPostAnalytics(accessToken: string, postUrn: string): Promise<LinkedInPostAnalytics> {\n if (!postUrn || !postUrn.startsWith('urn:li:')) {\n throw new SocialError('linkedin', `Invalid post URN: \"${postUrn}\". Expected format: urn:li:share:xxx or urn:li:ugcPost:xxx`, {\n statusCode: 400,\n errorCode: 'INVALID_URN',\n });\n }\n\n const encodedUrn = encodeURIComponent(postUrn);\n\n // TODO: Full impression/click analytics require the Marketing API with\n // r_organization_social scope and organizational entity share statistics endpoint.\n // This implementation uses the socialMetadata endpoint which provides reaction\n // and comment counts available via the community management API.\n const data: any = await this._api(\n 'GET',\n `${REST_API_BASE}/socialMetadata/${encodedUrn}`,\n accessToken,\n );\n\n const reactionSummaries: any[] = data?.reactionSummaries || [];\n const totalReactions = reactionSummaries.reduce(\n (sum: number, r: any) => sum + (r.count || 0),\n 0,\n );\n\n const likeCount = reactionSummaries.find((r: any) => r.reactionType === 'LIKE')?.count ?? null;\n const commentCount = data?.commentSummary?.totalFirstLevelComments ?? data?.totalComments ?? null;\n const shareCount = data?.totalShares ?? null;\n\n return {\n impressionCount: null, // Requires Marketing API\n clickCount: null, // Requires Marketing API\n likeCount: likeCount !== null ? likeCount : (totalReactions > 0 ? totalReactions : null),\n commentCount,\n shareCount,\n engagementRate: null, // Requires Marketing API impression data to compute\n };\n }\n\n // ─── Organizations ──────────────────────────────────────────────────\n\n /**\n * Get organizations the authenticated user administers.\n * Used to select a company page for posting as an organization.\n */\n async getOrganizations(accessToken: string): Promise<LinkedInOrganization[]> {\n try {\n const data: any = await this._api(\n 'GET',\n `${REST_API_BASE}/organizationAcls?q=roleAssignee&role=ADMINISTRATOR&projection=(elements*(organization~(id,localizedName,vanityName,logoV2(original~:playableStreams))))`,\n accessToken,\n );\n\n const elements: any[] = data?.elements || [];\n return elements.map((el: any) => {\n const org = el['organization~'] || {};\n const logoStreams = org.logoV2?.['original~']?.elements;\n const logoUrl = logoStreams?.[0]?.identifiers?.[0]?.identifier;\n\n return {\n id: org.id ? String(org.id) : '',\n name: org.localizedName || '',\n vanityName: org.vanityName || '',\n logoUrl: logoUrl || undefined,\n };\n });\n } catch (error: any) {\n if (error instanceof SocialError) throw error;\n throw new SocialError('linkedin', `Failed to get organizations: ${error.message}`, { originalError: error });\n }\n }\n\n // ─── Multi-Image Post ──────────────────────────────────────────────\n\n /**\n * Create a post with multiple images.\n * Registers N image uploads, uploads N binaries, then creates a multi-image post.\n *\n * @param accessToken - OAuth access token\n * @param authorUrn - Person or Organization URN\n * @param text - Post commentary text\n * @param imageBuffers - Array of image binaries\n * @param options - Optional alt texts and visibility\n */\n async createMultiImagePost(\n accessToken: string,\n authorUrn: string,\n text: string,\n imageBuffers: Buffer[],\n options: MultiImagePostOptions = {},\n ): Promise<any> {\n if (!imageBuffers.length) {\n throw new SocialError('linkedin', 'At least one image is required for multi-image post', { statusCode: 400 });\n }\n\n try {\n // Step 1: Register all image uploads in parallel\n const uploadRegistrations = await Promise.all(\n imageBuffers.map(() => this._registerImageUpload(accessToken, authorUrn)),\n );\n\n // Step 2: Upload all binaries in parallel\n await Promise.all(\n imageBuffers.map((buffer, i) =>\n this._uploadImageBinary(uploadRegistrations[i].uploadUrl, accessToken, buffer),\n ),\n );\n\n // Step 3: Create post with multi-image content\n const images = uploadRegistrations.map((reg, i) => ({\n id: reg.imageUrn,\n altText: options.altTexts?.[i] || '',\n }));\n\n const post = await this._api('POST', `${REST_API_BASE}/posts`, accessToken, {\n author: authorUrn,\n commentary: escapeLinkedInText(text).substring(0, 3000),\n visibility: options.visibility || 'PUBLIC',\n distribution: {\n feedDistribution: 'MAIN_FEED',\n targetEntities: [],\n thirdPartyDistributionChannels: [],\n },\n content: {\n multiImage: { images },\n },\n lifecycleState: 'PUBLISHED',\n });\n\n const postUrn = post?.['x-restli-id'] || post?.id || '';\n return {\n platformPostId: postUrn,\n platformUrl: postUrn ? `https://www.linkedin.com/feed/update/${encodeURIComponent(postUrn)}` : null,\n status: 'published',\n uploadedAt: new Date(),\n metadata: { authorUrn, imageCount: imageBuffers.length },\n };\n } catch (error: any) {\n if (error instanceof SocialError) throw error;\n throw new SocialError('linkedin', `Failed to create multi-image post: ${error.message}`, { originalError: error });\n }\n }\n\n // ─── Schema & Metadata ────────────────────────────────────────────────\n\n getCredentialZodSchema(): z.ZodType {\n return LinkedInCredentialsSchema;\n }\n\n getCredentialSchema(): CredentialField[] {\n return [\n {\n name: 'clientId',\n displayName: 'Client ID',\n type: 'text',\n required: true,\n description: 'OAuth2 Client ID from LinkedIn Developer Portal',\n },\n {\n name: 'clientSecret',\n displayName: 'Client Secret',\n type: 'password',\n required: true,\n description: 'OAuth2 Client Secret from LinkedIn Developer Portal',\n },\n ];\n }\n\n getMetadata(): ProviderMetadata {\n return {\n name: this.name,\n displayName: this.displayName,\n authType: this.authType,\n icon: 'linkedin',\n brandColor: '#0A66C2',\n description: 'Publish posts, articles, and videos on LinkedIn',\n scopes: this.scopes,\n scopeDescriptions: {\n openid: 'Sign in with LinkedIn (OpenID Connect)',\n profile: 'View your basic profile info (name, photo)',\n email: 'View your email address',\n w_member_social: 'Create, modify, and delete posts on your behalf',\n w_organization_social: 'Post on behalf of organizations/company pages you manage',\n },\n supportsScheduling: false,\n supportsEnvironment: false,\n setupGuide: [\n { step: 1, title: 'Create LinkedIn App', description: 'Go to linkedin.com/developers → \"Create App\". Fill in app name, LinkedIn Page, and logo' },\n { step: 2, title: 'Request Products', description: 'In your app\\'s \"Products\" tab, request access to \"Share on LinkedIn\" and \"Sign In with LinkedIn using OpenID Connect\"' },\n { step: 3, title: 'Configure OAuth', description: 'In the \"Auth\" tab, add the redirect URL shown below to \"Authorized redirect URLs for your app\"' },\n { step: 4, title: 'Copy Credentials', description: 'Copy \"Client ID\" and \"Client Secret\" from the \"Auth\" tab and paste them above' },\n { step: 5, title: 'Verify Scopes', description: 'Ensure your app has these OAuth 2.0 scopes: openid, profile, email, w_member_social, w_organization_social' },\n ],\n redirectUriPattern: this.defaultRedirectUri,\n credentialSchema: this.getCredentialSchema(),\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFA,MAAM,WAAW;AACjB,MAAM,YAAY;AAClB,MAAM,WAAW;AACjB,MAAM,gBAAgB;;;;;;;AAUtB,SAAS,mBAAmB,MAAsB;AAChD,KAAI,CAAC,KAAM,QAAO;AAClB,QAAO,KAAK,QAAQ,uBAAuB,SAAiB,OAAO,KAAK;;AAK1E,IAAa,mBAAb,cAAsC,iBAAiB;CACrD,AAAO;CACP,AAAO;CAEP,YAAY,MAAsB,EAAE,EAAE;AACpC,QAAM,IAAI;AACV,OAAK,OAAO;AACZ,OAAK,cAAc;AACnB,OAAK,WAAW;AAEhB,OAAK,qBACH,IAAI,eACJ,oBAAoB,IAAI,QAAQ,KAAK;AAEvC,OAAK,SAAS;GACZ;GACA;GACA;GACA;GACA;GACD;;;;;;;;;;;;CAeH,MAAM,KACJ,QACA,KACA,aACA,OAA4C,MAC5C,eAAuC,EAAE,EAC3B;EACd,MAAM,WAAW,gBAAgB;EACjC,MAAM,SAAS,MAAM,YAAiB,YAAY;GAChD,QAAQ,OAAO,aAAa;GAC5B;GACA,QAAQ;GACR,SAAS;IACP,oBAAoB;IACpB,6BAA6B;IAC7B,GAAG;IACJ;GACD,MAAM,CAAC,YAAY,OAAO,OAAO;GACjC,QAAQ,WAAW,IAAI,WAAW,KAAe,GAAG;GACpD,SAAS;GACT,OAAO,EAAE,UAAU,GAAG;GACtB,aAAa,KAAK,WAAW;AAC3B,QAAI,OAAO,OAAO,QAAQ,UAAU;KAClC,MAAM,IAAI;AACV,YAAO;MACL,SAAU,EAAE,WAAuB,EAAE,qBAAgC,uBAAuB,OAAO;MACnG,WAAY,EAAE,oBAAgD,EAAE,UAA4B;MAC7F;;AAEH,WAAO;;GAEV,CAAC;EAGF,MAAM,OAAO,OAAO;EACpB,MAAM,WAAW,OAAO,QAAQ,IAAI,cAAc;AAClD,MAAI,YAAY,QAAQ,OAAO,SAAS,SACtC,CAAC,KAAiC,iBAAiB;AAErD,SAAO;;;;;CAQT,WAAW,OAAe,cAAyD,EAAE,EAAE,UAAmC;AASxH,SAAO,GAAG,SAAS,GARJ,IAAI,gBAAgB;GACjC,eAAe;GACf,WAAW,YAAY;GACvB,cAAc,YAAY,eAAe,KAAK;GAC9C,OAAO,KAAK,OAAO,KAAK,IAAI;GAC5B;GACD,CAAC,CAE2B,UAAU;;;;;CAMzC,MAAM,aAAa,MAAc,cAAyD,EAAE,EAAwB;EAClH,MAAM,OAAO,IAAI,gBAAgB;GAC/B,YAAY;GACZ;GACA,WAAW,YAAY;GACvB,eAAe,YAAY;GAC3B,cAAc,YAAY,eAAe,KAAK;GAC/C,CAAC;EAQF,MAAM,OAAY,OAND,MAAM,MAAM,WAAW;GACtC,QAAQ;GACR,SAAS,EAAE,gBAAgB,qCAAqC;GAChE,MAAM,KAAK,UAAU;GACtB,CAAC,EAE+B,MAAM;AAEvC,MAAI,KAAK,MACP,OAAM,IAAI,YAAY,YAAY,KAAK,qBAAqB,KAAK,MAAM;AAGzE,SAAO;GACL,cAAc,KAAK;GACnB,eAAe,KAAK,iBAAiB;GACrC,YAAY,KAAK;GACjB,0BAA0B,KAAK,4BAA4B;GAC3D,OAAO,KAAK;GACZ,YAAY,KAAK,cAAc;GAChC;;;;;CAMH,MAAM,aAAa,cAAsB,cAAyD,EAAE,EAAwB;EAC1H,MAAM,OAAO,IAAI,gBAAgB;GAC/B,YAAY;GACZ,eAAe;GACf,WAAW,YAAY;GACvB,eAAe,YAAY;GAC5B,CAAC;EAQF,MAAM,OAAY,OAND,MAAM,MAAM,WAAW;GACtC,QAAQ;GACR,SAAS,EAAE,gBAAgB,qCAAqC;GAChE,MAAM,KAAK,UAAU;GACtB,CAAC,EAE+B,MAAM;AAEvC,MAAI,KAAK,MACP,OAAM,IAAI,YAAY,YAAY,KAAK,qBAAqB,uBAAuB;AAGrF,SAAO;GACL,cAAc,KAAK;GACnB,eAAe,KAAK,iBAAiB;GACrC,YAAY,KAAK;GACjB,0BAA0B,KAAK,4BAA4B;GAC3D,YAAY,KAAK,cAAc;GAChC;;;;;CAQH,MAAM,eAAe,aAA2C;EAC9D,MAAM,UAAe,MAAM,KAAK,KAAK,OAAO,GAAG,SAAS,YAAY,YAAY;AAEhF,SAAO;GACL,IAAI,QAAQ;GACZ,MAAM,QAAQ;GACd,OAAO,QAAQ;GACf,cAAc,QAAQ;GACvB;;;;;CAMH,MAAM,eAAe,gBAA6D;AAChF,MAAI;AACF,OAAI,eAAe,gBAAgB;IACjC,MAAM,YAA+B,OAAO,eAAe,mBAAmB,WAC1E,KAAK,MAAM,eAAe,eAAe,GACzC,eAAe;IACnB,MAAM,cAAc,MAAM,KAAK,eAAe,UAAU,aAAa;AAErE,WAAO;KACL,QAAQ;KACR,SAAS,gBAAgB,YAAY;KACrC,MAAM;MACJ,WAAW,YAAY;MACvB,cAAc,YAAY;MAC1B,cAAc,YAAY;MAC3B;KACF;;AAGH,OAAI,CAAC,eAAe,YAAY,CAAC,eAAe,aAC9C,QAAO;IACL,QAAQ;IACR,SAAS;IACV;AAGH,UAAO;IACL,QAAQ;IACR,SAAS;IACV;WACM,OAAY;AACnB,UAAO;IACL,QAAQ;IACR,SAAS,MAAM,WAAW;IAC3B;;;;;;;;;;CAaL,MAAM,eACJ,aACA,WACA,MACA,UAA2B,EAAE,EACf;EACd,MAAM,aAAa,QAAQ,cAAc;AAEzC,SAAO,KAAK,KAAK,QAAQ,GAAG,cAAc,SAAS,aAAa;GAC9D,QAAQ;GACR,YAAY,mBAAmB,KAAK,CAAC,UAAU,GAAG,IAAK;GACvD;GACA,cAAc;IACZ,kBAAkB;IAClB,gBAAgB,EAAE;IAClB,gCAAgC,EAAE;IACnC;GACD,gBAAgB;GACjB,CAAC;;;;;;;;;;CAWJ,MAAM,kBACJ,aACA,WACA,MACA,YACA,UAA8B,EAAE,EAClB;EACd,MAAM,UAA+B,EACnC,SAAS,EACP,QAAQ,YACT,EACF;AAED,MAAI,QAAQ,MAAO,SAAQ,QAAQ,QAAQ,QAAQ;AACnD,MAAI,QAAQ,YAAa,SAAQ,QAAQ,cAAc,QAAQ;AAE/D,SAAO,KAAK,KAAK,QAAQ,GAAG,cAAc,SAAS,aAAa;GAC9D,QAAQ;GACR,YAAY,mBAAmB,KAAK,CAAC,UAAU,GAAG,IAAK;GACvD,YAAY,QAAQ,cAAc;GAClC,cAAc;IACZ,kBAAkB;IAClB,gBAAgB,EAAE;IAClB,gCAAgC,EAAE;IACnC;GACD;GACA,gBAAgB;GACjB,CAAC;;;;;;;;CASJ,MAAM,qBAAqB,aAAqB,UAA8C;EAC5F,MAAM,OAAY,MAAM,KAAK,KAAK,QAAQ,GAAG,cAAc,kCAAkC,aAAa,EACxG,yBAAyB,EACvB,OAAO,UACR,EACF,CAAC;AAEF,SAAO;GACL,WAAW,KAAK,MAAM;GACtB,UAAU,KAAK,MAAM;GACtB;;;;;;;;;CAUH,MAAM,mBAAmB,WAAmB,aAAqB,aAAoC;AACnG,QAAM,KAAK,KAAK,OAAO,WAAW,aAAa,aAAa,EAC1D,gBAAgB,4BACjB,CAAC;;;;;;;;;;CAWJ,MAAM,gBACJ,aACA,WACA,MACA,aACA,UAA4B,EAAE,EAChB;EAEd,MAAM,EAAE,WAAW,aAAa,MAAM,KAAK,qBAAqB,aAAa,UAAU;AAGvF,QAAM,KAAK,mBAAmB,WAAW,aAAa,YAAY;AAGlE,SAAO,KAAK,KAAK,QAAQ,GAAG,cAAc,SAAS,aAAa;GAC9D,QAAQ;GACR,YAAY,mBAAmB,KAAK,CAAC,UAAU,GAAG,IAAK;GACvD,YAAY,QAAQ,cAAc;GAClC,cAAc;IACZ,kBAAkB;IAClB,gBAAgB,EAAE;IAClB,gCAAgC,EAAE;IACnC;GACD,SAAS,EACP,OAAO;IACL,IAAI;IACJ,OAAO,QAAQ,WAAW;IAC3B,EACF;GACD,gBAAgB;GACjB,CAAC;;;;;;;;;CAUJ,MAAM,qBACJ,aACA,UACA,eAC4B;EAC5B,MAAM,OAAY,MAAM,KAAK,KAAK,QAAQ,GAAG,cAAc,kCAAkC,aAAa,EACxG,yBAAyB;GACvB,OAAO;GACP;GACA,gBAAgB;GAChB,iBAAiB;GAClB,EACF,CAAC;AAEF,SAAO;GACL,WAAW,KAAK,MAAM,mBAAmB,IAAI;GAC7C,UAAU,KAAK,MAAM;GACtB;;;;;;;;CASH,MAAM,YAAY,QAAqD;EACrE,MAAM,EACJ,aACA,QAAQ,IACR,cAAc,IACd,QACA,eACE;EAEJ,MAAM,cAAc,OAAO;EAC3B,MAAM,YAAY,OAAO,mBAAmB,iBAAiB,OAAO;AAEpE,MAAI,CAAC,eAAe,CAAC,YAAY,OAC/B,OAAM,IAAI,YAAY,YAAY,sDAAsD;AAG1F,MAAI,WAAY,YAAW,EAAE;EAG7B,MAAM,EAAE,WAAW,aAAa,MAAM,KAAK,qBACzC,aACA,WACA,YAAY,OACb;AAED,MAAI,WAAY,YAAW,GAAG;AAG9B,QAAM,KAAK,KAAK,OAAO,WAAW,aAAa,aAAa,EAC1D,gBAAgB,4BACjB,CAAC;AAEF,MAAI,WAAY,YAAW,GAAG;EAG9B,MAAM,aAAa,mBAAmB,CAAC,OAAO,YAAY,CAAC,OAAO,QAAQ,CAAC,KAAK,OAAO,CAAC,CAAC,UAAU,GAAG,IAAK;EAE3G,MAAM,OAAY,MAAM,KAAK,KAAK,QAAQ,GAAG,cAAc,SAAS,aAAa;GAC/E,QAAQ;GACR;GACA,YAAY;GACZ,cAAc;IACZ,kBAAkB;IAClB,gBAAgB,EAAE;IAClB,gCAAgC,EAAE;IACnC;GACD,SAAS,EACP,OAAO,EAAE,IAAI,UAAU,EACxB;GACD,gBAAgB;GACjB,CAAC;AAEF,MAAI,WAAY,YAAW,IAAI;EAG/B,MAAM,UAAU,OAAO,kBAAkB,MAAM,MAAM;AAErD,SAAO;GACL,iBAAiB;GACjB,aAAa,wCAAwC,mBAAmB,QAAQ;GAChF,QAAQ;GACR,4BAAY,IAAI,MAAM;GACtB,UAAU;IACR;IACA;IACA;IACD;GACF;;;;;;;CAQH,MAAM,WAAW,aAAqB,SAA+B;AACnE,SAAO,KAAK,KAAK,UAAU,GAAG,cAAc,SAAS,mBAAmB,QAAQ,IAAI,YAAY;;;;;;;;;CAYlG,MAAM,QAAQ,aAAqB,SAAwC;AACzE,MAAI,CAAC,WAAW,CAAC,QAAQ,WAAW,UAAU,CAC5C,OAAM,IAAI,YAAY,YAAY,sBAAsB,QAAQ,6DAA6D;GAC3H,YAAY;GACZ,WAAW;GACZ,CAAC;EAGJ,MAAM,aAAa,mBAAmB,QAAQ;EAC9C,MAAM,OAAY,MAAM,KAAK,KAAK,OAAO,GAAG,cAAc,SAAS,cAAc,YAAY;AAE7F,SAAO;GACL,IAAI,KAAK,MAAM;GACf,WAAW,KAAK,UAAU;GAC1B,MAAM,KAAK,cAAc;GACzB,WAAW,KAAK,YAAY,IAAI,KAAK,KAAK,UAAU,CAAC,aAAa,GAAG;GACrE,YAAY,KAAK,cAAc;GAC/B,gBAAgB,KAAK,kBAAkB;GACvC,YAAY,KAAK;GACjB,UAAU,KAAK,KACX,wCAAwC,mBAAmB,KAAK,GAAG,KACnE;GACL;;;;;;;;;;;;;CAcH,MAAM,iBAAiB,aAAqB,SAAiD;AAC3F,MAAI,CAAC,WAAW,CAAC,QAAQ,WAAW,UAAU,CAC5C,OAAM,IAAI,YAAY,YAAY,sBAAsB,QAAQ,6DAA6D;GAC3H,YAAY;GACZ,WAAW;GACZ,CAAC;EAGJ,MAAM,aAAa,mBAAmB,QAAQ;EAM9C,MAAM,OAAY,MAAM,KAAK,KAC3B,OACA,GAAG,cAAc,kBAAkB,cACnC,YACD;EAED,MAAM,oBAA2B,MAAM,qBAAqB,EAAE;EAC9D,MAAM,iBAAiB,kBAAkB,QACtC,KAAa,MAAW,OAAO,EAAE,SAAS,IAC3C,EACD;EAED,MAAM,YAAY,kBAAkB,MAAM,MAAW,EAAE,iBAAiB,OAAO,EAAE,SAAS;EAC1F,MAAM,eAAe,MAAM,gBAAgB,2BAA2B,MAAM,iBAAiB;EAC7F,MAAM,aAAa,MAAM,eAAe;AAExC,SAAO;GACL,iBAAiB;GACjB,YAAY;GACZ,WAAW,cAAc,OAAO,YAAa,iBAAiB,IAAI,iBAAiB;GACnF;GACA;GACA,gBAAgB;GACjB;;;;;;CASH,MAAM,iBAAiB,aAAsD;AAC3E,MAAI;AAQF,YAPkB,MAAM,KAAK,KAC3B,OACA,GAAG,cAAc,2JACjB,YACD,GAE6B,YAAY,EAAE,EAC5B,KAAK,OAAY;IAC/B,MAAM,MAAM,GAAG,oBAAoB,EAAE;IAErC,MAAM,WADc,IAAI,SAAS,cAAc,YACjB,IAAI,cAAc,IAAI;AAEpD,WAAO;KACL,IAAI,IAAI,KAAK,OAAO,IAAI,GAAG,GAAG;KAC9B,MAAM,IAAI,iBAAiB;KAC3B,YAAY,IAAI,cAAc;KAC9B,SAAS,WAAW;KACrB;KACD;WACK,OAAY;AACnB,OAAI,iBAAiB,YAAa,OAAM;AACxC,SAAM,IAAI,YAAY,YAAY,gCAAgC,MAAM,WAAW,EAAE,eAAe,OAAO,CAAC;;;;;;;;;;;;;CAgBhH,MAAM,qBACJ,aACA,WACA,MACA,cACA,UAAiC,EAAE,EACrB;AACd,MAAI,CAAC,aAAa,OAChB,OAAM,IAAI,YAAY,YAAY,uDAAuD,EAAE,YAAY,KAAK,CAAC;AAG/G,MAAI;GAEF,MAAM,sBAAsB,MAAM,QAAQ,IACxC,aAAa,UAAU,KAAK,qBAAqB,aAAa,UAAU,CAAC,CAC1E;AAGD,SAAM,QAAQ,IACZ,aAAa,KAAK,QAAQ,MACxB,KAAK,mBAAmB,oBAAoB,GAAG,WAAW,aAAa,OAAO,CAC/E,CACF;GAGD,MAAM,SAAS,oBAAoB,KAAK,KAAK,OAAO;IAClD,IAAI,IAAI;IACR,SAAS,QAAQ,WAAW,MAAM;IACnC,EAAE;GAEH,MAAM,OAAO,MAAM,KAAK,KAAK,QAAQ,GAAG,cAAc,SAAS,aAAa;IAC1E,QAAQ;IACR,YAAY,mBAAmB,KAAK,CAAC,UAAU,GAAG,IAAK;IACvD,YAAY,QAAQ,cAAc;IAClC,cAAc;KACZ,kBAAkB;KAClB,gBAAgB,EAAE;KAClB,gCAAgC,EAAE;KACnC;IACD,SAAS,EACP,YAAY,EAAE,QAAQ,EACvB;IACD,gBAAgB;IACjB,CAAC;GAEF,MAAM,UAAU,OAAO,kBAAkB,MAAM,MAAM;AACrD,UAAO;IACL,gBAAgB;IAChB,aAAa,UAAU,wCAAwC,mBAAmB,QAAQ,KAAK;IAC/F,QAAQ;IACR,4BAAY,IAAI,MAAM;IACtB,UAAU;KAAE;KAAW,YAAY,aAAa;KAAQ;IACzD;WACM,OAAY;AACnB,OAAI,iBAAiB,YAAa,OAAM;AACxC,SAAM,IAAI,YAAY,YAAY,sCAAsC,MAAM,WAAW,EAAE,eAAe,OAAO,CAAC;;;CAMtH,yBAAoC;AAClC,SAAO;;CAGT,sBAAyC;AACvC,SAAO,CACL;GACE,MAAM;GACN,aAAa;GACb,MAAM;GACN,UAAU;GACV,aAAa;GACd,EACD;GACE,MAAM;GACN,aAAa;GACb,MAAM;GACN,UAAU;GACV,aAAa;GACd,CACF;;CAGH,cAAgC;AAC9B,SAAO;GACL,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,UAAU,KAAK;GACf,MAAM;GACN,YAAY;GACZ,aAAa;GACb,QAAQ,KAAK;GACb,mBAAmB;IACjB,QAAQ;IACR,SAAS;IACT,OAAO;IACP,iBAAiB;IACjB,uBAAuB;IACxB;GACD,oBAAoB;GACpB,qBAAqB;GACrB,YAAY;IACV;KAAE,MAAM;KAAG,OAAO;KAAuB,aAAa;KAA2F;IACjJ;KAAE,MAAM;KAAG,OAAO;KAAoB,aAAa;KAAyH;IAC5K;KAAE,MAAM;KAAG,OAAO;KAAmB,aAAa;KAAkG;IACpJ;KAAE,MAAM;KAAG,OAAO;KAAoB,aAAa;KAAiF;IACpI;KAAE,MAAM;KAAG,OAAO;KAAiB,aAAa;KAA8G;IAC/J;GACD,oBAAoB,KAAK;GACzB,kBAAkB,KAAK,qBAAqB;GAC7C"}