fix(bluesky): guard uploadMedia() against non-image HTTP responses
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>
This commit is contained in:
@@ -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": [],
|
||||
|
||||
119
scripts/patch-bluesky-syndicator-media-type-guard.mjs
Normal file
119
scripts/patch-bluesky-syndicator-media-type-guard.mjs
Normal file
@@ -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}`);
|
||||
Reference in New Issue
Block a user