← back to Marketing Command Center
chore: lint+refactor, fix channels postLinkedIn video routing, vN (session close)
60ffd0e80acf1ec3fb3c043bbcb6f11ab7aac81b · 2026-07-30 15:28:45 -0700 · Steve
- channels postLinkedIn: route videoUrl → /rest/videos (init+multipart PUT+finalize)
instead of the images endpoint; add Content-Type octet-stream to image PUT.
(lint-flagged critical: 'video' in mediaTypes exposed the image-only path.)
- linkedin/index.js: drop dead ETag case-fallback (Fetch headers are case-insensitive).
- version bump.
Files touched
M modules/channels/index.jsM modules/linkedin/index.jsM package.json
Diff
commit 60ffd0e80acf1ec3fb3c043bbcb6f11ab7aac81b
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 30 15:28:45 2026 -0700
chore: lint+refactor, fix channels postLinkedIn video routing, vN (session close)
- channels postLinkedIn: route videoUrl → /rest/videos (init+multipart PUT+finalize)
instead of the images endpoint; add Content-Type octet-stream to image PUT.
(lint-flagged critical: 'video' in mediaTypes exposed the image-only path.)
- linkedin/index.js: drop dead ETag case-fallback (Fetch headers are case-insensitive).
- version bump.
---
modules/channels/index.js | 51 ++++++++++++++++++++++++++++++++++++-----------
modules/linkedin/index.js | 2 +-
package.json | 2 +-
3 files changed, 41 insertions(+), 14 deletions(-)
diff --git a/modules/channels/index.js b/modules/channels/index.js
index b7ad9f2..3a18d4d 100644
--- a/modules/channels/index.js
+++ b/modules/channels/index.js
@@ -657,20 +657,47 @@ async function postLinkedIn(content) {
if (!token || !author) return [{ ok: false, error: 'LinkedIn not configured (token + org/author URN)' }];
const H = { Authorization: `Bearer ${token}`, 'LinkedIn-Version': '202401', 'X-Restli-Protocol-Version': '2.0.0' };
let mediaId = null;
+ // videoUrl → native VIDEO upload (videos endpoint + multipart PUT + finalize);
+ // mediaUrl → single-PUT IMAGE upload. Routing image vs video must match the
+ // media kind or LinkedIn rejects it (initializeUpload endpoints are distinct).
+ const isVideo = !!content.videoUrl && !content.mediaUrl;
const src = content.mediaUrl || content.videoUrl;
if (src) {
- const init = await fetch('https://api.linkedin.com/rest/images?action=initializeUpload', {
- method: 'POST', headers: { ...H, 'Content-Type': 'application/json' },
- body: JSON.stringify({ initializeUploadRequest: { owner: author } }),
- });
- const ij = await init.json().catch(() => ({}));
- const up = ij.value && ij.value.uploadUrl; mediaId = ij.value && ij.value.image;
- if (!init.ok || !up) return [{ ok: false, error: `image init ${init.status}: ${JSON.stringify(ij).slice(0, 150)}` }];
- const img = await fetch(src);
- if (!img.ok) return [{ ok: false, error: `couldn't fetch media (${img.status})` }];
- const buf = Buffer.from(await img.arrayBuffer());
- const put = await fetch(up, { method: 'PUT', headers: { Authorization: `Bearer ${token}` }, body: buf });
- if (!put.ok) return [{ ok: false, error: `image upload ${put.status}` }];
+ const media = await fetch(src);
+ if (!media.ok) return [{ ok: false, error: `couldn't fetch media (${media.status})` }];
+ const buf = Buffer.from(await media.arrayBuffer());
+ if (isVideo) {
+ const init = await fetch('https://api.linkedin.com/rest/videos?action=initializeUpload', {
+ method: 'POST', headers: { ...H, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ initializeUploadRequest: { owner: author, fileSizeBytes: buf.length, uploadCaptions: false, uploadThumbnail: false } }),
+ });
+ const ij = await init.json().catch(() => ({}));
+ const v = ij.value;
+ if (!init.ok || !v || !v.video) return [{ ok: false, error: `video init ${init.status}: ${JSON.stringify(ij).slice(0, 150)}` }];
+ mediaId = v.video;
+ const etags = [];
+ for (const ins of (v.uploadInstructions || [])) {
+ const first = Number(ins.firstByte), last = Number(ins.lastByte);
+ const put = await fetch(ins.uploadUrl, { method: 'PUT', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/octet-stream' }, body: buf.subarray(first, last + 1) });
+ if (!put.ok) return [{ ok: false, error: `video upload part ${put.status}` }];
+ etags.push(put.headers.get('etag'));
+ }
+ const fin = await fetch('https://api.linkedin.com/rest/videos?action=finalizeUpload', {
+ method: 'POST', headers: { ...H, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ finalizeUploadRequest: { video: mediaId, uploadToken: v.uploadToken || '', uploadedPartIds: etags } }),
+ });
+ if (!fin.ok) return [{ ok: false, error: `video finalize ${fin.status}` }];
+ } else {
+ const init = await fetch('https://api.linkedin.com/rest/images?action=initializeUpload', {
+ method: 'POST', headers: { ...H, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ initializeUploadRequest: { owner: author } }),
+ });
+ const ij = await init.json().catch(() => ({}));
+ const up = ij.value && ij.value.uploadUrl; mediaId = ij.value && ij.value.image;
+ if (!init.ok || !up) return [{ ok: false, error: `image init ${init.status}: ${JSON.stringify(ij).slice(0, 150)}` }];
+ const put = await fetch(up, { method: 'PUT', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/octet-stream' }, body: buf });
+ if (!put.ok) return [{ ok: false, error: `image upload ${put.status}` }];
+ }
}
const body = {
author, commentary: content.caption || '', visibility: 'PUBLIC',
diff --git a/modules/linkedin/index.js b/modules/linkedin/index.js
index 22988c8..8dd709d 100644
--- a/modules/linkedin/index.js
+++ b/modules/linkedin/index.js
@@ -70,7 +70,7 @@ async function liUploadMedia({ token, author, kind, ref }) {
const first = Number(ins.firstByte), last = Number(ins.lastByte);
const pr = await fetch(ins.uploadUrl, { method: 'PUT', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/octet-stream' }, body: bytes.subarray(first, last + 1) });
if (!pr.ok) throw new Error(`video PUT part ${pr.status}`);
- etags.push(pr.headers.get('etag') || pr.headers.get('ETag'));
+ etags.push(pr.headers.get('etag'));
}
const fr = await fetch('https://api.linkedin.com/rest/videos?action=finalizeUpload', {
method: 'POST', headers: { ...liHeaders(token), 'Content-Type': 'application/json' },
diff --git a/package.json b/package.json
index 306067c..5ff3b70 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "marketing-command-center",
- "version": "1.7.1",
+ "version": "1.8.0",
"description": "DW Marketing Command Center — Constant Contact, marketing calendar, suggested copy, on-demand layouts",
"main": "server.js",
"scripts": {
← ef6776b linkedin(mcc): incorporate native image/video posting into t
·
back to Marketing Command Center
·
fix(linkedin): blank media title default + video-wins when b 37441d3 →