From b4fc7ffb4f55a21d2e185dc32f700a1444303244 Mon Sep 17 00:00:00 2001 From: Sven Date: Sun, 29 Mar 2026 09:53:46 +0200 Subject: [PATCH] fix(bluesky): guard uploadMedia() against non-image HTTP responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- package.json | 4 +- ...ch-bluesky-syndicator-media-type-guard.mjs | 119 ++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 scripts/patch-bluesky-syndicator-media-type-guard.mjs diff --git a/package.json b/package.json index 022e51b5..e7179267 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs", - "serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", + "postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs", + "serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/scripts/patch-bluesky-syndicator-media-type-guard.mjs b/scripts/patch-bluesky-syndicator-media-type-guard.mjs new file mode 100644 index 00000000..33406f3f --- /dev/null +++ b/scripts/patch-bluesky-syndicator-media-type-guard.mjs @@ -0,0 +1,119 @@ +/** + * 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}`);