linkedin-agent 1.0.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.
@@ -0,0 +1,422 @@
1
+ "use strict";
2
+ /**
3
+ * LinkedIn Developer App automation via Playwright
4
+ *
5
+ * Selector Strategy (resilience to UI changes):
6
+ * 1. Semantic class names (.editable-block__update-btn, .editable-list__add-btn)
7
+ * 2. Stable IDs (#createAppNameInput, #clientIdInput)
8
+ * 3. ARIA/role attributes (role="combobox", aria-label)
9
+ * 4. Icon types (li-icon[type="pencil-icon"])
10
+ * 5. Placeholder text as last resort
11
+ *
12
+ * Input Strategy:
13
+ * - Always use keyboard.type() for Ember app compatibility
14
+ * - Use Playwright native click() instead of evaluate-based clicks
15
+ *
16
+ * Never rely on:
17
+ * - Ember dynamic IDs (#ember41, #ember79, etc.)
18
+ * - Text content (language-dependent)
19
+ * - Element index/position without context
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.createDevApp = createDevApp;
23
+ exports.extractCredentials = extractCredentials;
24
+ exports.configureRedirectUri = configureRedirectUri;
25
+ exports.requestProducts = requestProducts;
26
+ exports.verifyScopes = verifyScopes;
27
+ exports.setupDevApp = setupDevApp;
28
+ const browser_1 = require("./browser");
29
+ // ---------------------------------------------------------------------------
30
+ // Helpers
31
+ // ---------------------------------------------------------------------------
32
+ /**
33
+ * Try multiple selectors in order, return the first one that matches.
34
+ * This provides resilience when LinkedIn changes one selector but not others.
35
+ */
36
+ async function findElement(page, selectors, description) {
37
+ for (const selector of selectors) {
38
+ const loc = page.locator(selector).first();
39
+ if ((await loc.count()) > 0 && (await loc.isVisible().catch(() => false))) {
40
+ return loc;
41
+ }
42
+ }
43
+ throw new Error(`Could not find: ${description}. Tried: ${selectors.join(", ")}`);
44
+ }
45
+ /**
46
+ * Wait for page to settle (network idle + short delay).
47
+ */
48
+ async function settle(page, ms = 1500) {
49
+ await page.waitForLoadState("networkidle").catch(() => { });
50
+ await page.waitForTimeout(ms);
51
+ }
52
+ /**
53
+ * Type into an input field using keyboard.type() for Ember compatibility.
54
+ * Clears existing value first.
55
+ */
56
+ async function typeInto(page, selector, value) {
57
+ const input = page.locator(selector).first();
58
+ await input.click();
59
+ await page.keyboard.press("Meta+a");
60
+ await page.keyboard.press("Backspace");
61
+ await page.keyboard.type(value, { delay: 30 });
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // Step 1: Create Developer App
65
+ // ---------------------------------------------------------------------------
66
+ const DEFAULT_COMPANY_PAGE = "https://www.linkedin.com/company/103290544/";
67
+ async function createDevApp(page, logoPath, companyPageUrl) {
68
+ console.log("📝 Creating Developer App...");
69
+ await page.goto("https://www.linkedin.com/developers/apps/new", { waitUntil: "domcontentloaded" });
70
+ await settle(page);
71
+ const url = page.url();
72
+ if (!url.includes("/developers/apps")) {
73
+ throw new Error(`Unexpected redirect: ${url}. Login may be required.`);
74
+ }
75
+ // App name — stable semantic ID, fallback to placeholder
76
+ const appName = `social-agent-${Date.now().toString(36)}`;
77
+ const nameInput = await findElement(page, [
78
+ "#createAppNameInput",
79
+ 'input[placeholder*="app name" i]',
80
+ ], "App name input");
81
+ await nameInput.click();
82
+ await page.keyboard.type(appName, { delay: 30 });
83
+ console.log(` App name: ${appName}`);
84
+ // LinkedIn Page — typeahead accepts company name or page URL
85
+ const companyQuery = companyPageUrl || DEFAULT_COMPANY_PAGE;
86
+ const companyInput = await findElement(page, [
87
+ "#associateCompanyTypeaheadInput",
88
+ 'input[role="combobox"]',
89
+ 'input[placeholder*="company" i]',
90
+ ], "Company typeahead input");
91
+ await companyInput.click();
92
+ await page.keyboard.type(companyQuery, { delay: 20 });
93
+ await page.waitForTimeout(3000);
94
+ // Company may auto-select (shown as a card) or show a dropdown.
95
+ // Check if already selected first (card with dismiss button appears, input disappears).
96
+ const alreadySelected = await page.evaluate(() => {
97
+ // A selected company shows as a card — the typeahead input gets hidden/removed
98
+ const input = document.querySelector("#associateCompanyTypeaheadInput, input[role='combobox']");
99
+ if (!input || !input.offsetParent)
100
+ return true; // input gone = selected
101
+ // Also check for a dismiss/remove button next to company name
102
+ const dismissBtn = document.querySelector("button[aria-label*='dismiss' i], button[aria-label*='remove' i]");
103
+ if (dismissBtn)
104
+ return true;
105
+ return false;
106
+ });
107
+ if (alreadySelected) {
108
+ console.log(` LinkedIn Page: auto-selected from ${companyQuery}`);
109
+ }
110
+ else {
111
+ // Select first typeahead result from dropdown
112
+ const typeaheadOption = await findElement(page, [
113
+ '[role="option"]',
114
+ ".basic-typeahead__selectable",
115
+ ".search-typeahead-v2__hit",
116
+ '[role="listbox"] li',
117
+ ], "Typeahead option");
118
+ await typeaheadOption.click();
119
+ await page.waitForTimeout(500);
120
+ console.log(` LinkedIn Page: ${companyQuery}`);
121
+ }
122
+ // Privacy policy URL — stable semantic ID, fallback to placeholder
123
+ const privacyInput = await findElement(page, [
124
+ "#createAppPrivacyUrlInput",
125
+ 'input[placeholder*="http:// or https://" i]',
126
+ ], "Privacy policy URL input");
127
+ await privacyInput.click();
128
+ await page.keyboard.type("https://example.com/privacy", { delay: 30 });
129
+ console.log(" Privacy URL: set.");
130
+ // App logo — file input
131
+ const logoInput = await findElement(page, [
132
+ 'input[type="file"]',
133
+ "#appLogoImageUpload",
134
+ ], "Logo file input");
135
+ await logoInput.setInputFiles(logoPath);
136
+ await page.waitForTimeout(1000);
137
+ console.log(" Logo: uploaded.");
138
+ // Terms checkbox — click the <label> (Ember checkboxes intercept pointer on input)
139
+ const termsLabel = page.locator('label:has-text("agree")').first();
140
+ if ((await termsLabel.count()) > 0) {
141
+ await termsLabel.click();
142
+ }
143
+ else {
144
+ // Fallback: click any checkbox label near "terms"
145
+ await page.evaluate(() => {
146
+ const labels = Array.from(document.querySelectorAll("label"));
147
+ const target = labels.find((l) => l.textContent?.toLowerCase().includes("agree"));
148
+ target?.click();
149
+ });
150
+ }
151
+ console.log(" Terms: accepted.");
152
+ // Submit — data-control-name is stable, fallback to form submit button
153
+ const createBtn = await findElement(page, [
154
+ '[data-control-name="create_app_form_save_btn"]',
155
+ 'button[type="submit"][form="createAppForm"]',
156
+ 'button[type="submit"]:has-text("Create")',
157
+ ], "Create app button");
158
+ await createBtn.click();
159
+ await settle(page, 5000);
160
+ // Extract app ID from redirect URL
161
+ const afterUrl = page.url();
162
+ const appIdMatch = afterUrl.match(/\/apps\/(\d+)/);
163
+ if (!appIdMatch) {
164
+ throw new Error(`App creation may have failed. URL: ${afterUrl}`);
165
+ }
166
+ console.log(` ✅ App created! ID: ${appIdMatch[1]}\n`);
167
+ return appIdMatch[1];
168
+ }
169
+ // ---------------------------------------------------------------------------
170
+ // Step 2: Extract Credentials
171
+ // ---------------------------------------------------------------------------
172
+ async function extractCredentials(page, appId) {
173
+ console.log("🔑 Extracting credentials...");
174
+ await page.goto(`https://www.linkedin.com/developers/apps/${appId}/auth`, { waitUntil: "domcontentloaded" });
175
+ await settle(page);
176
+ // Client ID — stable semantic ID
177
+ const clientId = await page.locator("#clientIdInput").first().inputValue().catch(async () => {
178
+ // Fallback: find input near "Client ID" label
179
+ return page.evaluate(() => {
180
+ const labels = Array.from(document.querySelectorAll("label, span, div"));
181
+ for (const label of labels) {
182
+ if (label.textContent?.trim() === "Client ID:") {
183
+ const input = label.parentElement?.querySelector("input");
184
+ if (input)
185
+ return input.value;
186
+ }
187
+ }
188
+ return "";
189
+ });
190
+ });
191
+ // Client Secret — stable semantic ID, need to reveal first
192
+ let clientSecret = "";
193
+ const secretInput = page.locator("#primaryClientSecret").first();
194
+ if ((await secretInput.count()) > 0) {
195
+ clientSecret = await secretInput.inputValue();
196
+ }
197
+ if (!clientId)
198
+ throw new Error("Could not extract Client ID");
199
+ if (!clientSecret)
200
+ throw new Error("Could not extract Client Secret");
201
+ console.log(` Client ID: ${clientId}`);
202
+ console.log(" Client Secret: ******* (stored)");
203
+ console.log(" ✅ Credentials extracted.\n");
204
+ return { clientId, clientSecret };
205
+ }
206
+ // ---------------------------------------------------------------------------
207
+ // Step 3: Configure Redirect URI
208
+ // ---------------------------------------------------------------------------
209
+ async function configureRedirectUri(page, appId, redirectUri) {
210
+ console.log("🔧 Configuring redirect URI...");
211
+ await page.goto(`https://www.linkedin.com/developers/apps/${appId}/auth`, { waitUntil: "domcontentloaded" });
212
+ await settle(page);
213
+ // Check if URI already exists
214
+ const alreadyConfigured = await page.evaluate((uri) => {
215
+ return document.body.textContent?.includes(uri) ?? false;
216
+ }, redirectUri);
217
+ if (alreadyConfigured) {
218
+ console.log(` Already configured: ${redirectUri}`);
219
+ console.log(" ✅ Skipped.\n");
220
+ return;
221
+ }
222
+ // Find the editable block containing "redirect" and click its edit button
223
+ // Strategy: find the section by heading text, then find the pencil button inside the same form/block
224
+ const editClicked = await page.evaluate(() => {
225
+ // Find all form/editable-block containers
226
+ const blocks = Array.from(document.querySelectorAll("form"));
227
+ for (const block of blocks) {
228
+ const heading = block.querySelector("h3, h4");
229
+ if (heading?.textContent?.toLowerCase().includes("redirect")) {
230
+ // Find edit button: look for pencil icon button or edit-btn class
231
+ const editBtn = block.querySelector(".editable-block__edit-btn--enabled") ||
232
+ block.querySelector('.editable-block__edit-btn button') ||
233
+ block.querySelector('button:has(li-icon[type="pencil-icon"])') ||
234
+ block.querySelector("button");
235
+ if (editBtn) {
236
+ editBtn.click();
237
+ return true;
238
+ }
239
+ }
240
+ }
241
+ return false;
242
+ });
243
+ if (!editClicked)
244
+ throw new Error("Could not find redirect URI edit button");
245
+ await page.waitForTimeout(1000);
246
+ console.log(" Edit mode entered.");
247
+ // Click "+ Add redirect URL" — semantic class is most stable
248
+ const addBtn = await findElement(page, [
249
+ ".editable-list__add-btn",
250
+ 'button:has(li-icon[type="plus-icon"])',
251
+ ], "Add redirect URL button");
252
+ await addBtn.click();
253
+ await page.waitForTimeout(1000);
254
+ console.log(" Add button clicked.");
255
+ // Fill the input — scope to editable-list container
256
+ const input = await findElement(page, [
257
+ '.editable-list__entry-container input[type="text"]',
258
+ 'input[placeholder*="http"]',
259
+ ], "Redirect URL input");
260
+ await input.click();
261
+ await page.keyboard.type(redirectUri, { delay: 30 });
262
+ await page.waitForTimeout(500);
263
+ console.log(` Typed: ${redirectUri}`);
264
+ // Click Update — semantic class
265
+ const updateBtn = await findElement(page, [
266
+ ".editable-block__update-btn",
267
+ 'button[type="submit"]:visible',
268
+ ], "Update button");
269
+ await updateBtn.click();
270
+ await settle(page, 3000);
271
+ // Verify save succeeded (no Cancel button means we exited edit mode)
272
+ const stillEditing = (await page.locator(".editable-block__cancel-btn").count()) > 0;
273
+ if (stillEditing) {
274
+ throw new Error("Redirect URI save failed — still in edit mode");
275
+ }
276
+ console.log(" ✅ Redirect URI saved.\n");
277
+ }
278
+ // ---------------------------------------------------------------------------
279
+ // Step 4: Request Products
280
+ // ---------------------------------------------------------------------------
281
+ async function requestProducts(page, appId, productNames) {
282
+ console.log("📦 Requesting products...");
283
+ for (const productName of productNames) {
284
+ await page.goto(`https://www.linkedin.com/developers/apps/${appId}/products`, { waitUntil: "domcontentloaded" });
285
+ await settle(page);
286
+ // Check if product is already in "Added products" section
287
+ const alreadyAdded = await page.evaluate((name) => {
288
+ // "Added products" section appears at the top when products are added
289
+ const addedSection = Array.from(document.querySelectorAll("h2, h3")).find((h) => h.textContent?.toLowerCase().includes("added"));
290
+ if (addedSection) {
291
+ const parent = addedSection.closest("div, section");
292
+ if (parent?.textContent?.includes(name))
293
+ return true;
294
+ }
295
+ return false;
296
+ }, productName);
297
+ if (alreadyAdded) {
298
+ console.log(` ${productName}: already added.`);
299
+ continue;
300
+ }
301
+ // Find the product card and its "Request access" button
302
+ // Strategy: find h2 with product name, walk up to card, find button
303
+ const clicked = await page.evaluate((name) => {
304
+ const headings = Array.from(document.querySelectorAll("h2, h3, h4"));
305
+ for (const h of headings) {
306
+ if (h.textContent?.trim() === name) {
307
+ // Walk up to find the product card container
308
+ let el = h;
309
+ for (let i = 0; i < 8 && el; i++) {
310
+ el = el.parentElement;
311
+ if (el) {
312
+ const btn = el.querySelector("button");
313
+ if (btn?.textContent?.trim() === "Request access") {
314
+ btn.click();
315
+ return true;
316
+ }
317
+ }
318
+ }
319
+ }
320
+ }
321
+ return false;
322
+ }, productName);
323
+ if (!clicked) {
324
+ console.log(` ${productName}: not found or no "Request access" button.`);
325
+ continue;
326
+ }
327
+ await page.waitForTimeout(2000);
328
+ // Handle modal (terms agreement)
329
+ const modal = page.locator('[role="dialog"]').first();
330
+ if ((await modal.count()) > 0 && (await modal.isVisible())) {
331
+ // Accept terms — click label (avoid Ember checkbox interception)
332
+ const label = modal.locator("label").first();
333
+ if ((await label.count()) > 0) {
334
+ await label.click();
335
+ await page.waitForTimeout(300);
336
+ }
337
+ // Confirm — find submit/confirm button inside modal
338
+ const confirmClicked = await page.evaluate(() => {
339
+ const modal = document.querySelector('[role="dialog"]');
340
+ if (!modal)
341
+ return false;
342
+ const btns = Array.from(modal.querySelectorAll("button"));
343
+ const confirmBtn = btns.find((b) => {
344
+ const t = b.textContent?.trim() || "";
345
+ return t.includes("Request access") || t.includes("Submit") || t.includes("Agree");
346
+ });
347
+ if (confirmBtn) {
348
+ confirmBtn.click();
349
+ return true;
350
+ }
351
+ return false;
352
+ });
353
+ if (confirmClicked) {
354
+ await settle(page, 3000);
355
+ console.log(` ${productName}: ✅ requested.`);
356
+ }
357
+ else {
358
+ console.log(` ${productName}: ⚠️ modal appeared but could not confirm.`);
359
+ }
360
+ }
361
+ else {
362
+ console.log(` ${productName}: ✅ auto-approved (no modal).`);
363
+ }
364
+ }
365
+ console.log("");
366
+ }
367
+ // ---------------------------------------------------------------------------
368
+ // Step 5: Verify Scopes
369
+ // ---------------------------------------------------------------------------
370
+ async function verifyScopes(page, appId) {
371
+ console.log("🔍 Verifying OAuth scopes...");
372
+ await page.goto(`https://www.linkedin.com/developers/apps/${appId}/auth`, { waitUntil: "domcontentloaded" });
373
+ await settle(page);
374
+ const scopes = await page.evaluate(() => {
375
+ const results = [];
376
+ // Scopes are bold text (scope name) + description in the scopes section
377
+ const blocks = Array.from(document.querySelectorAll("div, section"));
378
+ for (const block of blocks) {
379
+ const heading = block.querySelector("h2, h3");
380
+ if (heading?.textContent?.toLowerCase().includes("scope")) {
381
+ // Find scope name elements (bold text)
382
+ const scopeEls = Array.from(block.querySelectorAll("b, strong, [class*='bold']"));
383
+ for (const el of scopeEls) {
384
+ const name = el.textContent?.trim() || "";
385
+ if (name && !name.includes("scope") && !name.includes("OAuth") && name.length < 30) {
386
+ results.push(name);
387
+ }
388
+ }
389
+ break;
390
+ }
391
+ }
392
+ return results;
393
+ });
394
+ if (scopes.length > 0) {
395
+ console.log(" Scopes:");
396
+ for (const s of scopes)
397
+ console.log(` ✅ ${s}`);
398
+ }
399
+ else {
400
+ console.log(" ⚠️ No scopes detected (products may need approval time).");
401
+ }
402
+ console.log("");
403
+ return scopes;
404
+ }
405
+ // ---------------------------------------------------------------------------
406
+ // Full Flow: Create + Configure
407
+ // ---------------------------------------------------------------------------
408
+ const REQUIRED_PRODUCTS = [
409
+ "Share on LinkedIn",
410
+ "Sign In with LinkedIn using OpenID Connect",
411
+ ];
412
+ const REDIRECT_URI = "http://localhost:3000/callback";
413
+ async function setupDevApp(context, logoPath, companyPageUrl) {
414
+ const page = context.pages()[0] || (await context.newPage());
415
+ await (0, browser_1.ensureLoggedIn)(page);
416
+ const appId = await createDevApp(page, logoPath, companyPageUrl);
417
+ const creds = await extractCredentials(page, appId);
418
+ await configureRedirectUri(page, appId, REDIRECT_URI);
419
+ await requestProducts(page, appId, REQUIRED_PRODUCTS);
420
+ await verifyScopes(page, appId);
421
+ return { appId, clientId: creds.clientId, clientSecret: creds.clientSecret };
422
+ }
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authenticate = exports.getValidCredentials = exports.loadCredentials = exports.deleteLinkedInPost = exports.editLinkedInPost = exports.postToLinkedIn = void 0;
4
+ // SDK — agent-friendly functions for programmatic use
5
+ var poster_1 = require("./poster");
6
+ Object.defineProperty(exports, "postToLinkedIn", { enumerable: true, get: function () { return poster_1.postToLinkedIn; } });
7
+ Object.defineProperty(exports, "editLinkedInPost", { enumerable: true, get: function () { return poster_1.editLinkedInPost; } });
8
+ Object.defineProperty(exports, "deleteLinkedInPost", { enumerable: true, get: function () { return poster_1.deleteLinkedInPost; } });
9
+ var auth_1 = require("./auth");
10
+ Object.defineProperty(exports, "loadCredentials", { enumerable: true, get: function () { return auth_1.loadCredentials; } });
11
+ Object.defineProperty(exports, "getValidCredentials", { enumerable: true, get: function () { return auth_1.getValidCredentials; } });
12
+ Object.defineProperty(exports, "authenticate", { enumerable: true, get: function () { return auth_1.authenticate; } });
package/dist/poster.js ADDED
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.postToLinkedIn = postToLinkedIn;
37
+ exports.editLinkedInPost = editLinkedInPost;
38
+ exports.deleteLinkedInPost = deleteLinkedInPost;
39
+ const https = __importStar(require("https"));
40
+ const auth_1 = require("./auth");
41
+ function httpsRequest(url, data, headers, method = "POST") {
42
+ return new Promise((resolve, reject) => {
43
+ const urlObj = new URL(url);
44
+ const reqHeaders = { ...headers };
45
+ if (data) {
46
+ reqHeaders["Content-Length"] = Buffer.byteLength(data);
47
+ }
48
+ const req = https.request({
49
+ hostname: urlObj.hostname,
50
+ path: urlObj.pathname + urlObj.search,
51
+ method,
52
+ headers: reqHeaders,
53
+ }, (res) => {
54
+ let body = "";
55
+ res.on("data", (chunk) => (body += chunk));
56
+ res.on("end", () => resolve({ status: res.statusCode || 0, body, headers: res.headers }));
57
+ });
58
+ req.on("error", reject);
59
+ if (data)
60
+ req.write(data);
61
+ req.end();
62
+ });
63
+ }
64
+ function makeHeaders(accessToken) {
65
+ return {
66
+ Authorization: `Bearer ${accessToken}`,
67
+ "Content-Type": "application/json",
68
+ "X-Restli-Protocol-Version": "2.0.0",
69
+ "LinkedIn-Version": "202601",
70
+ };
71
+ }
72
+ async function withRetry(fn, onSuccess) {
73
+ let creds = await (0, auth_1.getValidCredentials)();
74
+ let resp = await fn(creds);
75
+ if (resp.status === 401) {
76
+ console.log("Token expired. Refreshing...");
77
+ creds = await (0, auth_1.refreshAccessToken)(creds);
78
+ resp = await fn(creds);
79
+ }
80
+ if (resp.status >= 200 && resp.status < 300)
81
+ return onSuccess(resp);
82
+ return { success: false, error: `HTTP ${resp.status}: ${resp.body}` };
83
+ }
84
+ // ---------------------------------------------------------------------------
85
+ // Create
86
+ // ---------------------------------------------------------------------------
87
+ async function postToLinkedIn(options) {
88
+ return withRetry((creds) => {
89
+ const body = {
90
+ author: `urn:li:person:${creds.personId}`,
91
+ commentary: options.text,
92
+ visibility: "PUBLIC",
93
+ distribution: {
94
+ feedDistribution: "MAIN_FEED",
95
+ targetEntities: [],
96
+ thirdPartyDistributionChannels: [],
97
+ },
98
+ lifecycleState: "PUBLISHED",
99
+ isReshareDisabledByAuthor: false,
100
+ };
101
+ if (options.linkUrl) {
102
+ body.content = { article: { source: options.linkUrl } };
103
+ }
104
+ return httpsRequest("https://api.linkedin.com/rest/posts", JSON.stringify(body), makeHeaders(creds.accessToken));
105
+ }, (resp) => ({ success: true, postId: resp.headers["x-restli-id"] || "" }));
106
+ }
107
+ // ---------------------------------------------------------------------------
108
+ // Edit
109
+ // ---------------------------------------------------------------------------
110
+ async function editLinkedInPost(options) {
111
+ const encodedId = encodeURIComponent(options.postId);
112
+ return withRetry((creds) => httpsRequest(`https://api.linkedin.com/rest/posts/${encodedId}`, JSON.stringify({ patch: { $set: { commentary: options.text } } }), makeHeaders(creds.accessToken), "POST"), () => ({ success: true, postId: options.postId }));
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Delete
116
+ // ---------------------------------------------------------------------------
117
+ async function deleteLinkedInPost(postId) {
118
+ const encodedId = encodeURIComponent(postId);
119
+ return withRetry((creds) => httpsRequest(`https://api.linkedin.com/rest/posts/${encodedId}`, "", makeHeaders(creds.accessToken), "DELETE"), () => ({ success: true, postId }));
120
+ }