@venturekit-pro/social 0.0.0-dev.20260814232921 → 0.0.0-dev.20260815190251
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/facebook.d.ts +13 -5
- package/dist/adapters/facebook.d.ts.map +1 -1
- package/dist/adapters/facebook.js +155 -32
- package/dist/adapters/facebook.js.map +1 -1
- package/dist/adapters/linkedin.d.ts +17 -14
- package/dist/adapters/linkedin.d.ts.map +1 -1
- package/dist/adapters/linkedin.js +99 -75
- package/dist/adapters/linkedin.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +5 -5
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* Facebook (Pages) adapter — Meta Graph API.
|
|
3
3
|
*
|
|
4
4
|
* Endpoints:
|
|
5
|
-
* - Text post:
|
|
6
|
-
* -
|
|
5
|
+
* - Text post: `POST /v18.0/<page-id>/feed`
|
|
6
|
+
* - Single photo: `POST /v18.0/<page-id>/photos`
|
|
7
|
+
* - Multi-photo: `POST /v18.0/<page-id>/photos` (published=false)
|
|
8
|
+
* per photo, then `POST /v18.0/<page-id>/feed`
|
|
9
|
+
* with `attached_media` referencing all photo ids.
|
|
7
10
|
*
|
|
8
11
|
* Auth: Page access token via `?access_token=…` OR
|
|
9
12
|
* `Authorization: Bearer …`. We use the header form so the
|
|
@@ -20,9 +23,14 @@
|
|
|
20
23
|
* because the adapter downloads the image in-process (where it CAN
|
|
21
24
|
* reach a presigned / private URL) and pushes the bytes to Meta.
|
|
22
25
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
+
* Multi-photo posts (2–10 images) create an album-style feed post:
|
|
27
|
+
* each image is pre-uploaded to `/<page-id>/photos` with
|
|
28
|
+
* `published=false`, then a single `/<page-id>/feed` post references
|
|
29
|
+
* all uploaded photo ids via the `attached_media` field. This is the
|
|
30
|
+
* only Graph API flow that produces a multi-image post on a Page.
|
|
31
|
+
*
|
|
32
|
+
* Meta's /photos endpoint accepts JPEG and PNG only. Images in other
|
|
33
|
+
* formats (e.g. WebP, which the CMS uses for blog covers) are
|
|
26
34
|
* converted to JPEG before upload via the optional `sharp` peer
|
|
27
35
|
* dependency. The constraints allow WebP so validation doesn't block
|
|
28
36
|
* the post before the adapter can convert it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facebook.d.ts","sourceRoot":"","sources":["../../src/adapters/facebook.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"facebook.d.ts","sourceRoot":"","sources":["../../src/adapters/facebook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAIV,yBAAyB,EAE1B,MAAM,aAAa,CAAC;AAiBrB,eAAO,MAAM,oBAAoB,EAAE,yBAelC,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC,yBAAyB,CAAC,CAAC;CAClD;AAED,wBAAgB,qBAAqB,CACnC,MAAM,GAAE,qBAA0B,GACjC,aAAa,CAoIf"}
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* Facebook (Pages) adapter — Meta Graph API.
|
|
3
3
|
*
|
|
4
4
|
* Endpoints:
|
|
5
|
-
* - Text post:
|
|
6
|
-
* -
|
|
5
|
+
* - Text post: `POST /v18.0/<page-id>/feed`
|
|
6
|
+
* - Single photo: `POST /v18.0/<page-id>/photos`
|
|
7
|
+
* - Multi-photo: `POST /v18.0/<page-id>/photos` (published=false)
|
|
8
|
+
* per photo, then `POST /v18.0/<page-id>/feed`
|
|
9
|
+
* with `attached_media` referencing all photo ids.
|
|
7
10
|
*
|
|
8
11
|
* Auth: Page access token via `?access_token=…` OR
|
|
9
12
|
* `Authorization: Bearer …`. We use the header form so the
|
|
@@ -20,20 +23,26 @@
|
|
|
20
23
|
* because the adapter downloads the image in-process (where it CAN
|
|
21
24
|
* reach a presigned / private URL) and pushes the bytes to Meta.
|
|
22
25
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
+
* Multi-photo posts (2–10 images) create an album-style feed post:
|
|
27
|
+
* each image is pre-uploaded to `/<page-id>/photos` with
|
|
28
|
+
* `published=false`, then a single `/<page-id>/feed` post references
|
|
29
|
+
* all uploaded photo ids via the `attached_media` field. This is the
|
|
30
|
+
* only Graph API flow that produces a multi-image post on a Page.
|
|
31
|
+
*
|
|
32
|
+
* Meta's /photos endpoint accepts JPEG and PNG only. Images in other
|
|
33
|
+
* formats (e.g. WebP, which the CMS uses for blog covers) are
|
|
26
34
|
* converted to JPEG before upload via the optional `sharp` peer
|
|
27
35
|
* dependency. The constraints allow WebP so validation doesn't block
|
|
28
36
|
* the post before the adapter can convert it.
|
|
29
37
|
*/
|
|
30
|
-
import {
|
|
31
|
-
import { downloadMedia, ensureUploadableImage, filenameForMime, } from '../media.js';
|
|
38
|
+
import { SocialAuthError, SocialPublishError, SocialValidationError, } from '../types.js';
|
|
32
39
|
import { validatePost } from '../validation.js';
|
|
33
40
|
const META_GRAPH_BASE = 'https://graph.facebook.com';
|
|
34
41
|
const META_GRAPH_VERSION = 'v18.0';
|
|
35
42
|
/** MIME types Meta's /photos endpoint actually accepts. */
|
|
36
43
|
const FACEBOOK_UPLOAD_MIME = new Set(['image/jpeg', 'image/png']);
|
|
44
|
+
/** Maximum photos per album-style feed post (Meta's hard limit). */
|
|
45
|
+
const MAX_FB_PHOTOS = 10;
|
|
37
46
|
export const FACEBOOK_CONSTRAINTS = {
|
|
38
47
|
maxBodyChars: 63206, // Meta's published max; 2000 is the recommended ceiling
|
|
39
48
|
minBodyChars: 1,
|
|
@@ -42,7 +51,10 @@ export const FACEBOOK_CONSTRAINTS = {
|
|
|
42
51
|
// WebP is allowed at validation time so the adapter can convert it
|
|
43
52
|
// to JPEG before uploading — Meta's /photos endpoint rejects WebP.
|
|
44
53
|
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
45
|
-
|
|
54
|
+
// Facebook supports album-style posts with up to 10 photos via
|
|
55
|
+
// `attached_media` on the /feed endpoint. Single-photo posts still go
|
|
56
|
+
// through /photos; 2+ photos use the multi-upload flow.
|
|
57
|
+
maxMediaCount: 10,
|
|
46
58
|
allowedAspectRatios: ['1:1', '4:3', '16:9', '4:5'],
|
|
47
59
|
maxMediaSizeBytes: 10 * 1024 * 1024,
|
|
48
60
|
supportsFirstComment: false,
|
|
@@ -63,13 +75,13 @@ export function createFacebookAdapter(config = {}) {
|
|
|
63
75
|
},
|
|
64
76
|
async publish(post, credentials) {
|
|
65
77
|
const pageId = stripPagePrefix(credentials.authorRef);
|
|
66
|
-
const
|
|
78
|
+
const mediaCount = post.media?.length ?? 0;
|
|
67
79
|
// Text / link posts hit /<page-id>/feed — no image, so the
|
|
68
80
|
// original JSON body path is correct.
|
|
69
|
-
if (
|
|
81
|
+
if (mediaCount === 0) {
|
|
70
82
|
const endpoint = `/${version}/${encodeURIComponent(pageId)}/feed`;
|
|
71
83
|
const body = buildFeedBody(post);
|
|
72
|
-
const res = await
|
|
84
|
+
const res = await fbFetch('facebook', `${baseUrl}${endpoint}`, {
|
|
73
85
|
method: 'POST',
|
|
74
86
|
headers: {
|
|
75
87
|
Authorization: `Bearer ${credentials.accessToken}`,
|
|
@@ -79,29 +91,67 @@ export function createFacebookAdapter(config = {}) {
|
|
|
79
91
|
});
|
|
80
92
|
return parseResult(res, post);
|
|
81
93
|
}
|
|
82
|
-
//
|
|
83
|
-
// We download the image in-process and upload the bytes via
|
|
84
|
-
// `source` field — Meta's servers don't need to fetch
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
// Single photo posts hit /<page-id>/photos with multipart form
|
|
95
|
+
// data. We download the image in-process and upload the bytes via
|
|
96
|
+
// the `source` field — Meta's servers don't need to fetch
|
|
97
|
+
// anything.
|
|
98
|
+
if (mediaCount === 1) {
|
|
99
|
+
const media = post.media[0];
|
|
100
|
+
const endpoint = `/${version}/${encodeURIComponent(pageId)}/photos`;
|
|
101
|
+
const { imgBuf, mimeType } = await prepareImage(media);
|
|
102
|
+
const form = new FormData();
|
|
103
|
+
form.append('message', post.body);
|
|
104
|
+
form.append('source', new Blob([imgBuf], { type: mimeType }), 'cover.jpg');
|
|
105
|
+
// Do NOT set Content-Type manually — fetch() sets the multipart
|
|
106
|
+
// boundary automatically from FormData. A manual header would
|
|
107
|
+
// omit the boundary and Meta would reject the request.
|
|
108
|
+
const res = await fbFetch('facebook', `${baseUrl}${endpoint}`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { Authorization: `Bearer ${credentials.accessToken}` },
|
|
111
|
+
body: form,
|
|
112
|
+
});
|
|
113
|
+
return parseResult(res, post);
|
|
114
|
+
}
|
|
115
|
+
// Multi-photo posts (2–10 images) create an album-style feed
|
|
116
|
+
// post. Each image is pre-uploaded to /<page-id>/photos with
|
|
117
|
+
// `published=false` (multipart form data), then a single
|
|
118
|
+
// /<page-id>/feed post references all uploaded photo ids via
|
|
119
|
+
// `attached_media`.
|
|
120
|
+
const photosEndpoint = `/${version}/${encodeURIComponent(pageId)}/photos`;
|
|
121
|
+
const feedEndpoint = `/${version}/${encodeURIComponent(pageId)}/feed`;
|
|
122
|
+
const attachedMedia = [];
|
|
123
|
+
for (const media of post.media.slice(0, MAX_FB_PHOTOS)) {
|
|
124
|
+
const { imgBuf, mimeType } = await prepareImage(media);
|
|
125
|
+
const form = new FormData();
|
|
126
|
+
form.append('published', 'false');
|
|
127
|
+
form.append('source', new Blob([imgBuf], { type: mimeType }), 'image.jpg');
|
|
128
|
+
const uploadRes = await fbFetch('facebook', `${baseUrl}${photosEndpoint}`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { Authorization: `Bearer ${credentials.accessToken}` },
|
|
131
|
+
body: form,
|
|
132
|
+
});
|
|
133
|
+
const uploadJson = uploadRes.body;
|
|
134
|
+
const photoId = uploadJson?.id;
|
|
135
|
+
if (!photoId) {
|
|
136
|
+
throw new SocialPublishError('[social/facebook] Multi-photo upload returned no id; vendor body: ' +
|
|
137
|
+
uploadRes.bodyText.slice(0, 200), { platform: 'facebook', raw: uploadRes.body });
|
|
138
|
+
}
|
|
139
|
+
attachedMedia.push({ media_fbid: photoId });
|
|
140
|
+
}
|
|
141
|
+
// Create the feed post referencing all uploaded photos.
|
|
142
|
+
const feedBody = {
|
|
143
|
+
message: post.body,
|
|
144
|
+
attached_media: attachedMedia,
|
|
145
|
+
};
|
|
146
|
+
const feedRes = await fbFetch('facebook', `${baseUrl}${feedEndpoint}`, {
|
|
100
147
|
method: 'POST',
|
|
101
|
-
headers: {
|
|
102
|
-
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
150
|
+
'Content-Type': 'application/json',
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify(feedBody),
|
|
103
153
|
});
|
|
104
|
-
return parseResult(
|
|
154
|
+
return parseResult(feedRes, post);
|
|
105
155
|
},
|
|
106
156
|
};
|
|
107
157
|
}
|
|
@@ -131,4 +181,77 @@ function parseResult(res, post) {
|
|
|
131
181
|
raw: json,
|
|
132
182
|
};
|
|
133
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Download image bytes from a URL the calling process can reach
|
|
186
|
+
* (presigned S3, CDN, etc.) but the platform's servers cannot.
|
|
187
|
+
*/
|
|
188
|
+
async function downloadImage(url, platform) {
|
|
189
|
+
const res = await fetch(url);
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
throw new SocialPublishError(`[social/${platform}] Could not fetch media for upload: ` +
|
|
192
|
+
`${res.status} ${res.statusText} — ${url}`, { platform: platform, status: res.status });
|
|
193
|
+
}
|
|
194
|
+
return res.arrayBuffer();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Download an image and convert it to a MIME type Meta's /photos
|
|
198
|
+
* endpoint accepts (JPEG or PNG). Returns the raw bytes + the final
|
|
199
|
+
* MIME type. Used by both the single-photo and multi-photo flows.
|
|
200
|
+
*/
|
|
201
|
+
async function prepareImage(media) {
|
|
202
|
+
let imgBuf = new Uint8Array(await downloadImage(media.url, 'facebook'));
|
|
203
|
+
let mimeType = media.mimeType;
|
|
204
|
+
if (!FACEBOOK_UPLOAD_MIME.has(mimeType)) {
|
|
205
|
+
const sharp = await importSharp();
|
|
206
|
+
imgBuf = await sharp(imgBuf).jpeg({ quality: 90 }).toBuffer();
|
|
207
|
+
mimeType = 'image/jpeg';
|
|
208
|
+
}
|
|
209
|
+
return { imgBuf, mimeType };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Lazy-import `sharp` so it's only loaded when an image actually
|
|
213
|
+
* needs format conversion. The package declares it as an optional
|
|
214
|
+
* peer dependency — apps that only post JPEG/PNG never need it.
|
|
215
|
+
*/
|
|
216
|
+
async function importSharp() {
|
|
217
|
+
try {
|
|
218
|
+
const sharp = (await import('sharp')).default;
|
|
219
|
+
return sharp;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
throw new Error('[social/facebook] The `sharp` package is required to convert images to JPEG for Facebook. ' +
|
|
223
|
+
'Install it with `npm install sharp` or `pnpm add sharp`.');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* fetch wrapper with the same vendor-status → typed-error mapping as
|
|
228
|
+
* `vendorFetch`, but accepts `FormData` as the body (the shared
|
|
229
|
+
* `vendorFetch` types `body` as `string | URLSearchParams`).
|
|
230
|
+
*/
|
|
231
|
+
async function fbFetch(platform, url, opts) {
|
|
232
|
+
const res = await fetch(url, {
|
|
233
|
+
method: opts.method,
|
|
234
|
+
headers: { Accept: 'application/json', ...opts.headers },
|
|
235
|
+
body: opts.body,
|
|
236
|
+
});
|
|
237
|
+
const bodyText = await res.text();
|
|
238
|
+
let body;
|
|
239
|
+
try {
|
|
240
|
+
body = bodyText ? JSON.parse(bodyText) : null;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
body = bodyText;
|
|
244
|
+
}
|
|
245
|
+
if (res.ok)
|
|
246
|
+
return { body, bodyText };
|
|
247
|
+
const truncated = bodyText.length > 1024 ? bodyText.slice(0, 1024) + '…' : bodyText;
|
|
248
|
+
const message = `${res.status} ${res.statusText}: ${truncated}`;
|
|
249
|
+
if (res.status === 401 || res.status === 403) {
|
|
250
|
+
throw new SocialAuthError(message, { platform, status: res.status, raw: body });
|
|
251
|
+
}
|
|
252
|
+
if (res.status >= 400 && res.status < 500) {
|
|
253
|
+
throw new SocialValidationError(message, { platform, status: res.status, raw: body });
|
|
254
|
+
}
|
|
255
|
+
throw new SocialPublishError(message, { platform, status: res.status, raw: body });
|
|
256
|
+
}
|
|
134
257
|
//# sourceMappingURL=facebook.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facebook.js","sourceRoot":"","sources":["../../src/adapters/facebook.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"facebook.js","sourceRoot":"","sources":["../../src/adapters/facebook.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LinkedIn adapter —
|
|
2
|
+
* LinkedIn adapter — Posts API (REST).
|
|
3
3
|
*
|
|
4
|
-
* Endpoint: `POST https://api.linkedin.com/
|
|
4
|
+
* Endpoint: `POST https://api.linkedin.com/rest/posts`
|
|
5
5
|
* Auth: OAuth 2.0 bearer token in `Authorization: Bearer …`
|
|
6
6
|
* Author: `urn:li:organization:<id>` or `urn:li:person:<id>`
|
|
7
7
|
* — caller supplies via `credentials.authorRef`.
|
|
8
8
|
*
|
|
9
|
-
* The adapter implements text
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* The adapter implements text, image, multi-image, and link-preview
|
|
10
|
+
* posts. Link previews use `content.article` (the Posts API does NOT
|
|
11
|
+
* scrape URLs — `title` and `description` are required fields we
|
|
12
|
+
* derive from the body). CTA buttons (`contentCallToActionLabel`) only
|
|
13
|
+
* render on sponsored content (ads), not organic posts, so we don't
|
|
14
|
+
* use them.
|
|
13
15
|
*
|
|
14
16
|
* Image staging:
|
|
15
|
-
* If `media.url` is already a LinkedIn
|
|
16
|
-
* (`urn:li:
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* 1. `POST /
|
|
17
|
+
* If `media.url` is already a LinkedIn image URN
|
|
18
|
+
* (`urn:li:image:…`) — e.g. the caller pre-staged the upload — the
|
|
19
|
+
* adapter uses it directly. Otherwise the adapter performs LinkedIn's
|
|
20
|
+
* two-step staging automatically:
|
|
21
|
+
* 1. `POST /rest/images?action=initializeUpload` → upload URL + image URN
|
|
20
22
|
* 2. `PUT` the image bytes to the upload URL
|
|
21
|
-
* 3. Use the
|
|
23
|
+
* 3. Use the image URN in the post's `content.media` or
|
|
24
|
+
* `content.multiImage` field
|
|
22
25
|
* This means callers can pass a plain image URL (presigned S3, CDN,
|
|
23
26
|
* etc.) without implementing the staging protocol themselves.
|
|
24
27
|
*
|
|
25
|
-
* LinkedIn's
|
|
26
|
-
*
|
|
28
|
+
* LinkedIn's Images API accepts JPEG and PNG only (not WebP). The
|
|
29
|
+
* format is read from the downloaded bytes rather than
|
|
27
30
|
* `media.mimeType`, and anything else is converted to JPEG before
|
|
28
31
|
* upload. Conversion requires the optional `sharp` peer dependency;
|
|
29
32
|
* if it's not installed, the adapter throws a clear error.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linkedin.d.ts","sourceRoot":"","sources":["../../src/adapters/linkedin.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"linkedin.d.ts","sourceRoot":"","sources":["../../src/adapters/linkedin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAIV,yBAAyB,EAG1B,MAAM,aAAa,CAAC;AAWrB,eAAO,MAAM,oBAAoB,EAAE,yBAUlC,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,WAAW,CAAC,EAAE,OAAO,CAAC,yBAAyB,CAAC,CAAC;CAClD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,GAAE,qBAA0B,GACjC,aAAa,CA8Ef"}
|
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LinkedIn adapter —
|
|
2
|
+
* LinkedIn adapter — Posts API (REST).
|
|
3
3
|
*
|
|
4
|
-
* Endpoint: `POST https://api.linkedin.com/
|
|
4
|
+
* Endpoint: `POST https://api.linkedin.com/rest/posts`
|
|
5
5
|
* Auth: OAuth 2.0 bearer token in `Authorization: Bearer …`
|
|
6
6
|
* Author: `urn:li:organization:<id>` or `urn:li:person:<id>`
|
|
7
7
|
* — caller supplies via `credentials.authorRef`.
|
|
8
8
|
*
|
|
9
|
-
* The adapter implements text
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* The adapter implements text, image, multi-image, and link-preview
|
|
10
|
+
* posts. Link previews use `content.article` (the Posts API does NOT
|
|
11
|
+
* scrape URLs — `title` and `description` are required fields we
|
|
12
|
+
* derive from the body). CTA buttons (`contentCallToActionLabel`) only
|
|
13
|
+
* render on sponsored content (ads), not organic posts, so we don't
|
|
14
|
+
* use them.
|
|
13
15
|
*
|
|
14
16
|
* Image staging:
|
|
15
|
-
* If `media.url` is already a LinkedIn
|
|
16
|
-
* (`urn:li:
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* 1. `POST /
|
|
17
|
+
* If `media.url` is already a LinkedIn image URN
|
|
18
|
+
* (`urn:li:image:…`) — e.g. the caller pre-staged the upload — the
|
|
19
|
+
* adapter uses it directly. Otherwise the adapter performs LinkedIn's
|
|
20
|
+
* two-step staging automatically:
|
|
21
|
+
* 1. `POST /rest/images?action=initializeUpload` → upload URL + image URN
|
|
20
22
|
* 2. `PUT` the image bytes to the upload URL
|
|
21
|
-
* 3. Use the
|
|
23
|
+
* 3. Use the image URN in the post's `content.media` or
|
|
24
|
+
* `content.multiImage` field
|
|
22
25
|
* This means callers can pass a plain image URL (presigned S3, CDN,
|
|
23
26
|
* etc.) without implementing the staging protocol themselves.
|
|
24
27
|
*
|
|
25
|
-
* LinkedIn's
|
|
26
|
-
*
|
|
28
|
+
* LinkedIn's Images API accepts JPEG and PNG only (not WebP). The
|
|
29
|
+
* format is read from the downloaded bytes rather than
|
|
27
30
|
* `media.mimeType`, and anything else is converted to JPEG before
|
|
28
31
|
* upload. Conversion requires the optional `sharp` peer dependency;
|
|
29
32
|
* if it's not installed, the adapter throws a clear error.
|
|
@@ -65,37 +68,41 @@ export function createLinkedInAdapter(config = {}) {
|
|
|
65
68
|
},
|
|
66
69
|
async publish(post, credentials) {
|
|
67
70
|
// Stage images that aren't already LinkedIn URNs.
|
|
71
|
+
// When a link preview is active (post.link), media is dropped
|
|
72
|
+
// by the post-builder — so staging is skipped entirely.
|
|
68
73
|
const media = post.media ?? [];
|
|
69
74
|
const stagedMedia = [];
|
|
70
75
|
for (const m of media) {
|
|
71
|
-
if (m.url.startsWith('urn:li:
|
|
76
|
+
if (m.url.startsWith('urn:li:image:')) {
|
|
72
77
|
// Caller already staged the upload — use the URN directly.
|
|
73
78
|
stagedMedia.push(m.url);
|
|
74
79
|
}
|
|
75
80
|
else {
|
|
76
|
-
// Auto-stage: download →
|
|
81
|
+
// Auto-stage: download → initializeUpload → PUT bytes → URN.
|
|
77
82
|
stagedMedia.push(await stageImage(baseUrl, credentials, m, constraints.maxMediaSizeBytes));
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
|
-
|
|
85
|
+
// Use the Posts API (/rest/posts). The older UGC Posts API
|
|
86
|
+
// (/v2/ugcPosts) is legacy and doesn't support the current
|
|
87
|
+
// content model (article / multiImage).
|
|
88
|
+
const body = buildPostBody(post, credentials, stagedMedia);
|
|
81
89
|
const headers = {
|
|
82
90
|
Authorization: `Bearer ${credentials.accessToken}`,
|
|
83
91
|
'Content-Type': 'application/json',
|
|
84
92
|
'X-Restli-Protocol-Version': '2.0.0',
|
|
93
|
+
// Required by the Posts API — format is YYYYMM.
|
|
94
|
+
'Linkedin-Version': '202511',
|
|
85
95
|
};
|
|
86
|
-
|
|
87
|
-
// equivalent, and it cannot travel in the body either (see
|
|
88
|
-
// `UgcPostBody`). Retries are deduped caller-side.
|
|
89
|
-
const res = await vendorFetch('linkedin', `${baseUrl}/v2/ugcPosts`, {
|
|
96
|
+
const res = await vendorFetch('linkedin', `${baseUrl}/rest/posts`, {
|
|
90
97
|
method: 'POST',
|
|
91
98
|
headers,
|
|
92
99
|
body: JSON.stringify(body),
|
|
93
100
|
});
|
|
101
|
+
// The Posts API returns the post URN in the `x-restli-id` header
|
|
102
|
+
// (e.g. "urn:li:share:6844785523593134080") and an empty body.
|
|
94
103
|
const json = res.body;
|
|
95
|
-
const externalRef = json?.id ?? res.headers.get('x-linkedin-id') ?? '';
|
|
104
|
+
const externalRef = json?.id ?? res.headers.get('x-restli-id') ?? res.headers.get('x-linkedin-id') ?? '';
|
|
96
105
|
if (!externalRef) {
|
|
97
|
-
// LinkedIn always returns 201 with an id; if we didn't get
|
|
98
|
-
// one, the post probably WASN'T accepted — fail loud.
|
|
99
106
|
throw new Error('[social/linkedin] Publish returned no id; vendor body: ' + res.bodyText.slice(0, 200));
|
|
100
107
|
}
|
|
101
108
|
// First-comment companion thread. Best-effort: the post is
|
|
@@ -105,7 +112,6 @@ export function createLinkedInAdapter(config = {}) {
|
|
|
105
112
|
}
|
|
106
113
|
return {
|
|
107
114
|
externalRef,
|
|
108
|
-
// The public URL form is `https://www.linkedin.com/feed/update/<urn>`.
|
|
109
115
|
publishedUrl: `https://www.linkedin.com/feed/update/${encodeURIComponent(externalRef)}`,
|
|
110
116
|
publishedAt: new Date().toISOString(),
|
|
111
117
|
idempotencyKey: post.idempotencyKey,
|
|
@@ -117,61 +123,56 @@ export function createLinkedInAdapter(config = {}) {
|
|
|
117
123
|
}
|
|
118
124
|
// ─── Internal: image staging ──────────────────────────────────────────
|
|
119
125
|
/**
|
|
120
|
-
* Download an image from a URL, convert to JPEG if needed,
|
|
121
|
-
* the upload with LinkedIn, push the bytes, and return
|
|
126
|
+
* Download an image from a URL, convert to JPEG if needed, initialize
|
|
127
|
+
* the upload with LinkedIn's Images API, push the bytes, and return
|
|
128
|
+
* the image URN (`urn:li:image:...`).
|
|
129
|
+
*
|
|
130
|
+
* Uses the Images API (`POST /rest/images?action=initializeUpload`)
|
|
131
|
+
* which returns `urn:li:image:...` URNs compatible with the Posts API.
|
|
132
|
+
* The older Assets API returned `urn:li:digitalmediaAsset:...` URNs
|
|
133
|
+
* which the Posts API rejects.
|
|
122
134
|
*/
|
|
123
135
|
async function stageImage(baseUrl, credentials, media, maxBytes) {
|
|
124
136
|
// Download the image (the calling process can reach presigned /
|
|
125
137
|
// private URLs that LinkedIn's servers cannot), then transcode if
|
|
126
|
-
// the
|
|
138
|
+
// the image recipe won't take the format.
|
|
127
139
|
const downloaded = await downloadMedia(media.url, 'linkedin', { maxBytes });
|
|
128
140
|
const { bytes, mimeType } = await ensureUploadableImage(downloaded, media.mimeType, LINKEDIN_ACCEPTED_MIME, 'linkedin');
|
|
129
|
-
// Step 1:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
registerUploadRequest: {
|
|
133
|
-
recipes: ['urn:li:digitalmediaRecipe:feedshare-image'],
|
|
141
|
+
// Step 1: initialize the upload via the Images API.
|
|
142
|
+
const initBody = {
|
|
143
|
+
initializeUploadRequest: {
|
|
134
144
|
owner: credentials.authorRef,
|
|
135
|
-
serviceRelationships: [
|
|
136
|
-
{
|
|
137
|
-
relationshipType: 'OWNER',
|
|
138
|
-
identifier: 'urn:li:userGeneratedContent',
|
|
139
|
-
},
|
|
140
|
-
],
|
|
141
|
-
supportedUploadMechanism: ['SYNCHRONOUS_UPLOAD'],
|
|
142
145
|
},
|
|
143
146
|
};
|
|
144
|
-
const
|
|
147
|
+
const initRes = await vendorFetch('linkedin', `${baseUrl}/rest/images?action=initializeUpload`, {
|
|
145
148
|
method: 'POST',
|
|
146
149
|
headers: {
|
|
147
150
|
Authorization: `Bearer ${credentials.accessToken}`,
|
|
148
151
|
'Content-Type': 'application/json',
|
|
149
152
|
'X-Restli-Protocol-Version': '2.0.0',
|
|
153
|
+
'Linkedin-Version': '202511',
|
|
150
154
|
},
|
|
151
|
-
body: JSON.stringify(
|
|
155
|
+
body: JSON.stringify(initBody),
|
|
152
156
|
});
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
regRes.bodyText.slice(0, 500), { platform: 'linkedin', status: regRes.status, raw: regRes.body });
|
|
157
|
+
const initJson = initRes.body;
|
|
158
|
+
const uploadUrl = initJson?.value?.uploadUrl;
|
|
159
|
+
const imageUrn = initJson?.value?.image;
|
|
160
|
+
if (!uploadUrl || !imageUrn) {
|
|
161
|
+
throw new SocialPublishError('[social/linkedin] initializeUpload returned no upload URL / image URN; vendor body: ' +
|
|
162
|
+
initRes.bodyText.slice(0, 500), { platform: 'linkedin', status: initRes.status, raw: initRes.body });
|
|
160
163
|
}
|
|
161
164
|
// Step 2: upload the bytes. The upload URL is a one-shot signed
|
|
162
165
|
// endpoint rather than the LinkedIn API proper, but it still goes
|
|
163
166
|
// through `vendorFetch` so the call inherits the abort timeout and
|
|
164
|
-
// the status → typed-error mapping.
|
|
165
|
-
// asked us to echo back.
|
|
167
|
+
// the status → typed-error mapping.
|
|
166
168
|
await vendorFetch('linkedin', uploadUrl, {
|
|
167
169
|
method: 'PUT',
|
|
168
170
|
headers: {
|
|
169
171
|
'Content-Type': mimeType,
|
|
170
|
-
...(uploadMech?.headers ?? {}),
|
|
171
172
|
},
|
|
172
173
|
body: bytes,
|
|
173
174
|
});
|
|
174
|
-
return
|
|
175
|
+
return imageUrn;
|
|
175
176
|
}
|
|
176
177
|
// ─── Internal: first-comment ──────────────────────────────────────────
|
|
177
178
|
/**
|
|
@@ -203,36 +204,59 @@ async function postFirstComment(baseUrl, credentials, postUrn, text) {
|
|
|
203
204
|
// Swallowed — see doc comment.
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
|
-
function
|
|
207
|
+
function buildPostBody(post, credentials, stagedMedia) {
|
|
207
208
|
const hasMedia = stagedMedia.length > 0;
|
|
208
|
-
const
|
|
209
|
+
const hasLink = !!post.link;
|
|
210
|
+
const altTexts = post.media ?? [];
|
|
209
211
|
const body = {
|
|
210
212
|
author: credentials.authorRef,
|
|
213
|
+
commentary: post.body,
|
|
214
|
+
visibility: 'PUBLIC',
|
|
211
215
|
lifecycleState: 'PUBLISHED',
|
|
212
|
-
|
|
213
|
-
'
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
},
|
|
216
|
+
distribution: {
|
|
217
|
+
feedDistribution: 'MAIN_FEED',
|
|
218
|
+
targetEntities: [],
|
|
219
|
+
thirdPartyDistributionChannels: [],
|
|
217
220
|
},
|
|
218
|
-
|
|
221
|
+
isReshareDisabledByAuthor: false,
|
|
219
222
|
};
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
status: 'READY',
|
|
232
|
-
originalUrl: post.link,
|
|
223
|
+
if (hasLink && post.link) {
|
|
224
|
+
// Link preview post (content.article). LinkedIn can't combine
|
|
225
|
+
// a link preview with images, so media is dropped — same rule
|
|
226
|
+
// as Facebook. The Posts API does NOT scrape URLs, so title
|
|
227
|
+
// and description are required fields we derive from the body.
|
|
228
|
+
const firstLine = post.body.split('\n').find((l) => l.trim()) ?? post.body;
|
|
229
|
+
body.content = {
|
|
230
|
+
article: {
|
|
231
|
+
source: post.link,
|
|
232
|
+
title: firstLine.slice(0, 200),
|
|
233
|
+
description: post.body.slice(0, 400),
|
|
233
234
|
},
|
|
234
|
-
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
else if (hasMedia) {
|
|
238
|
+
if (stagedMedia.length === 1) {
|
|
239
|
+
// Single image — use content.media
|
|
240
|
+
body.content = {
|
|
241
|
+
media: {
|
|
242
|
+
id: stagedMedia[0],
|
|
243
|
+
...(altTexts[0]?.altText ? { title: altTexts[0].altText } : {}),
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// Multiple images (2-20) — use content.multiImage
|
|
249
|
+
body.content = {
|
|
250
|
+
multiImage: {
|
|
251
|
+
images: stagedMedia.map((urn, i) => ({
|
|
252
|
+
id: urn,
|
|
253
|
+
...(altTexts[i]?.altText ? { altText: altTexts[i].altText } : {}),
|
|
254
|
+
})),
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
235
258
|
}
|
|
259
|
+
// else: text-only post, no content field
|
|
236
260
|
return body;
|
|
237
261
|
}
|
|
238
262
|
//# sourceMappingURL=linkedin.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linkedin.js","sourceRoot":"","sources":["../../src/adapters/linkedin.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"linkedin.js","sourceRoot":"","sources":["../../src/adapters/linkedin.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -94,7 +94,7 @@ export interface SocialPost {
|
|
|
94
94
|
/**
|
|
95
95
|
* Caller-side idempotency token. This is NEVER sent to a vendor: none
|
|
96
96
|
* of the supported platforms expose an idempotency field, and
|
|
97
|
-
* LinkedIn's
|
|
97
|
+
* LinkedIn's Posts API rejects unknown body fields outright with
|
|
98
98
|
* `403 ACCESS_DENIED`. Adapters echo it back on `PublishResult` so the
|
|
99
99
|
* caller can dedupe retries against its own records.
|
|
100
100
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venturekit-pro/social",
|
|
3
|
-
"version": "0.0.0-dev.
|
|
3
|
+
"version": "0.0.0-dev.20260815190251",
|
|
4
4
|
"description": "Social-platform publishing adapters + catalog for VentureKit applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"publishing"
|
|
56
56
|
],
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@venturekit/core": "0.0.0-dev.
|
|
58
|
+
"@venturekit/core": "0.0.0-dev.20260815190251"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@venturekit/data": "0.0.0-dev.
|
|
61
|
+
"@venturekit/data": "0.0.0-dev.20260815190251",
|
|
62
62
|
"sharp": ">=0.32"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
|
-
"@types/node": "^26.1.
|
|
74
|
-
"@venturekit/data": "0.0.0-dev.
|
|
73
|
+
"@types/node": "^26.1.2",
|
|
74
|
+
"@venturekit/data": "0.0.0-dev.20260815190251",
|
|
75
75
|
"sharp": "^0.35.3",
|
|
76
76
|
"typescript": "^5.3.0"
|
|
77
77
|
},
|