uploadMedia() had no content-type check, so an HTML login-redirect response from an auth-protected internal endpoint was uploaded to Bluesky as a blob with encoding "text/html". uploadBlob() accepts it, but record validation rejects the post with 'Expected "image/*" (got "text/html")'. The patch mirrors the guard already present in uploadImageFromUrl() and also wraps per-photo uploads in try/catch so one bad photo doesn't abort the entire syndication — other photos and the post text are still published. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
120 lines
4.4 KiB
JavaScript
120 lines
4.4 KiB
JavaScript
/**
|
|
* Patch: guard uploadMedia() against non-image HTTP responses.
|
|
*
|
|
* Root cause:
|
|
* uploadMedia() fetches a photo URL and uploads whatever it receives to Bluesky
|
|
* without checking the Content-Type. If the internal fetch returns an HTML page
|
|
* (e.g. a login redirect from an auth-protected endpoint), the blob is uploaded
|
|
* with encoding "text/html". uploadBlob() accepts it, but app.bsky.feed.post
|
|
* record validation then rejects the post:
|
|
* "Expected 'image/*' (got 'text/html') at $.record.embed.images[0].image.mimeType"
|
|
*
|
|
* uploadImageFromUrl() has this guard already (it returns null for non-image
|
|
* responses) but uploadMedia() does not.
|
|
*
|
|
* Fix:
|
|
* 1. Add a content-type guard to uploadMedia() — throw if response is not image/*.
|
|
* 2. Wrap per-photo uploads in post() with try/catch and filter out failures,
|
|
* so one bad photo doesn't block the entire syndication.
|
|
*/
|
|
|
|
import { access, readFile, writeFile } from "node:fs/promises";
|
|
|
|
const TARGET =
|
|
"node_modules/@rmdes/indiekit-syndicator-bluesky/lib/bluesky.js";
|
|
|
|
const MARKER = "// bsky-media-type-guard patch";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 1. Guard in uploadMedia(): reject non-image content types
|
|
// ---------------------------------------------------------------------------
|
|
const OLD_ENCODING = ` let blob = await mediaResponse.blob();
|
|
let encoding = mediaResponse.headers.get("Content-Type");
|
|
|
|
if (encoding?.startsWith("image/")) {`;
|
|
|
|
const NEW_ENCODING = ` let blob = await mediaResponse.blob();
|
|
let encoding = mediaResponse.headers.get("Content-Type");
|
|
|
|
// Reject non-image responses (e.g. HTML login redirects) ${MARKER}
|
|
if (!encoding || !encoding.startsWith("image/")) {
|
|
throw new Error(\`uploadMedia: non-image content-type "\${encoding}" for \${mediaUrl}\`); ${MARKER}
|
|
}
|
|
|
|
if (encoding?.startsWith("image/")) {`;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2. Per-photo error handling in post(): skip failed uploads instead of
|
|
// propagating a broken blob into the images array.
|
|
// ---------------------------------------------------------------------------
|
|
const OLD_UPLOADS = ` if (properties.photo) {
|
|
const photos = properties.photo.slice(0, 4);
|
|
const uploads = photos.map(async (photo) => ({
|
|
alt: photo.alt || "",
|
|
image: await this.uploadMedia(photo, me),
|
|
}));
|
|
images = await Promise.all(uploads);
|
|
}`;
|
|
|
|
const NEW_UPLOADS = ` if (properties.photo) {
|
|
const photos = properties.photo.slice(0, 4);
|
|
const uploads = photos.map(async (photo) => { ${MARKER}
|
|
try {
|
|
const image = await this.uploadMedia(photo, me);
|
|
return image ? { alt: photo.alt || "", image } : null; ${MARKER}
|
|
} catch (err) {
|
|
console.error(\`[Bluesky] uploadMedia failed for \${photo.url}: \${err.message}\`); ${MARKER}
|
|
return null; ${MARKER}
|
|
}
|
|
});
|
|
images = (await Promise.all(uploads)).filter(Boolean); ${MARKER}
|
|
}`;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function exists(filePath) {
|
|
try {
|
|
await access(filePath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!(await exists(TARGET))) {
|
|
console.log("[postinstall] patch-bluesky-syndicator-media-type-guard: target not found");
|
|
process.exit(0);
|
|
}
|
|
|
|
const source = await readFile(TARGET, "utf8");
|
|
|
|
if (source.includes(MARKER)) {
|
|
console.log("[postinstall] patch-bluesky-syndicator-media-type-guard: already applied");
|
|
process.exit(0);
|
|
}
|
|
|
|
let updated = source;
|
|
let changed = false;
|
|
|
|
if (updated.includes(OLD_ENCODING)) {
|
|
updated = updated.replace(OLD_ENCODING, NEW_ENCODING);
|
|
changed = true;
|
|
} else {
|
|
console.warn("[postinstall] patch-bluesky-syndicator-media-type-guard: encoding block not found — skipping");
|
|
}
|
|
|
|
if (updated.includes(OLD_UPLOADS)) {
|
|
updated = updated.replace(OLD_UPLOADS, NEW_UPLOADS);
|
|
changed = true;
|
|
} else {
|
|
console.warn("[postinstall] patch-bluesky-syndicator-media-type-guard: uploads block not found — skipping");
|
|
}
|
|
|
|
if (!changed || updated === source) {
|
|
console.log("[postinstall] patch-bluesky-syndicator-media-type-guard: no changes applied");
|
|
process.exit(0);
|
|
}
|
|
|
|
await writeFile(TARGET, updated, "utf8");
|
|
console.log(`[postinstall] Applied patch-bluesky-syndicator-media-type-guard to ${TARGET}`);
|