From 733c00b1b38d10ba75e167cdd6b443cb7576f19a Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 16 Mar 2026 20:24:46 +0100 Subject: [PATCH] fix(patches): rewrite micropub self-fetch to localhost for jailed setup Node can't reach its own public HTTPS URL (ECONNREFUSED 127.0.0.1:443) because port 443 only exists on the nginx jail. Rewrite self-referential fetch URLs to http://localhost:3000 in endpoint-posts, endpoint-syndicate, and endpoint-share. Co-Authored-By: Claude Opus 4.6 --- package.json | 4 +- .../patch-endpoint-posts-fetch-diagnostic.mjs | 217 +++++++++++++----- scripts/patch-micropub-fetch-internal-url.mjs | 113 +++++++++ 3 files changed, 271 insertions(+), 63 deletions(-) create mode 100644 scripts/patch-micropub-fetch-internal-url.mjs diff --git a/package.json b/package.json index 5734ab2a..19bdbe3c 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-posts-ai-fields.mjs && node scripts/patch-endpoint-posts-ai-cleanup.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-micropub-ai-block-resync.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-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-prefill-url.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-endpoint-posts-search-tags.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-inbox-skip-view-activity-parse.mjs && node scripts/patch-inbox-ignore-view-activity.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-ap-normalize-nested-tags.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-posts-ai-fields.mjs && node scripts/patch-endpoint-posts-ai-cleanup.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-micropub-ai-block-resync.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-microsub-reader-ap-dispatch.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-prefill-url.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-endpoint-posts-search-tags.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-inbox-skip-view-activity-parse.mjs && node scripts/patch-inbox-ignore-view-activity.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-ap-normalize-nested-tags.mjs && node 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-posts-ai-fields.mjs && node scripts/patch-endpoint-posts-ai-cleanup.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-micropub-ai-block-resync.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-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-prefill-url.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-endpoint-posts-search-tags.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-inbox-skip-view-activity-parse.mjs && node scripts/patch-inbox-ignore-view-activity.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-ap-normalize-nested-tags.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-posts-ai-fields.mjs && node scripts/patch-endpoint-posts-ai-cleanup.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-micropub-ai-block-resync.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-microsub-reader-ap-dispatch.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-prefill-url.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-endpoint-posts-search-tags.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-inbox-skip-view-activity-parse.mjs && node scripts/patch-inbox-ignore-view-activity.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-ap-normalize-nested-tags.mjs && node 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-endpoint-posts-fetch-diagnostic.mjs b/scripts/patch-endpoint-posts-fetch-diagnostic.mjs index 35bf694d..183898d9 100644 --- a/scripts/patch-endpoint-posts-fetch-diagnostic.mjs +++ b/scripts/patch-endpoint-posts-fetch-diagnostic.mjs @@ -1,8 +1,25 @@ +/** + * Patch: rewrite self-referential fetch URLs to use localhost and add + * diagnostic logging for fetch failures. + * + * When behind a reverse proxy (e.g. nginx in a separate FreeBSD jail), + * the endpoint-posts form controller fetches the micropub endpoint via + * the public URL (https://...). But the Node process doesn't listen on + * 443 — only nginx does. This causes ECONNREFUSED on the Node jail. + * + * Fix: rewrite the URL to http://localhost: before fetching, so + * the request stays inside the Node jail. The public URL is preserved + * for everything else (HTML link headers, external clients, etc.). + * + * Controlled by INTERNAL_FETCH_URL env var (e.g. "http://localhost:3000"). + * Falls back to http://localhost:${PORT || 3000} automatically. + */ + import { access, readFile, writeFile } from "node:fs/promises"; const filePath = "node_modules/@indiekit/endpoint-posts/lib/endpoint.js"; -const marker = "// [patch] fetch-diagnostic"; +const marker = "// [patch] fetch-internal-rewrite"; async function exists(p) { try { @@ -14,28 +31,71 @@ async function exists(p) { } if (!(await exists(filePath))) { - console.log("[postinstall] endpoint-posts endpoint.js not found — skipping fetch-diagnostic patch"); + console.log("[postinstall] endpoint-posts endpoint.js not found — skipping fetch-rewrite patch"); process.exit(0); } const source = await readFile(filePath, "utf8"); if (source.includes(marker)) { - console.log("[postinstall] endpoint-posts fetch-diagnostic patch already applied"); + console.log("[postinstall] endpoint-posts fetch-rewrite patch already applied"); process.exit(0); } -// Wrap the fetch calls to log the underlying cause on failure -const oldPost = ` async post(url, accessToken, jsonBody = false) { - const endpointResponse = await fetch(url, {`; +// Also handle the case where the old diagnostic-only patch was applied +const oldMarker = "// [patch] fetch-diagnostic"; +let cleanSource = source; +if (cleanSource.includes(oldMarker)) { + // Strip old patch — we'll re-apply from scratch on the original structure. + // Safest approach: bail and let the user re-run after npm install. + console.log("[postinstall] Old fetch-diagnostic patch detected — stripping before re-patching"); + // We can't cleanly reverse the old patch, so we need to check if the + // original structure is still recognisable. If not, warn and skip. +} -const newPost = ` ${marker} +const original = `import { IndiekitError } from "@indiekit/error"; + +export const endpoint = { + /** + * Micropub query + * @param {string} url - URL + * @param {string} accessToken - Access token + * @returns {Promise} Response data + */ + async get(url, accessToken) { + const endpointResponse = await fetch(url, { + headers: { + accept: "application/json", + authorization: \`Bearer \${accessToken}\`, + }, + }); + + if (!endpointResponse.ok) { + throw await IndiekitError.fromFetch(endpointResponse); + } + + const body = await endpointResponse.json(); + + return body; + }, + + /** + * Micropub action + * @param {string} url - URL + * @param {string} accessToken - Access token + * @param {object} [jsonBody] - JSON body + * @returns {Promise} Response data + */ async post(url, accessToken, jsonBody = false) { - let endpointResponse; - try { - endpointResponse = await fetch(url, {`; - -const oldPostEnd = ` }); + const endpointResponse = await fetch(url, { + method: "POST", + headers: { + accept: "application/json", + authorization: \`Bearer \${accessToken}\`, + ...(jsonBody && { "content-type": "application/json" }), + }, + ...(jsonBody && { body: JSON.stringify(jsonBody) }), + }); if (!endpointResponse.ok) { throw await IndiekitError.fromFetch(endpointResponse); @@ -47,11 +107,78 @@ const oldPostEnd = ` }); }, };`; -const newPostEnd = ` }); +const patched = `import { IndiekitError } from "@indiekit/error"; + +${marker} +const _internalBase = (() => { + if (process.env.INTERNAL_FETCH_URL) return process.env.INTERNAL_FETCH_URL.replace(/\\/+$/, ""); + const port = process.env.PORT || "3000"; + return \`http://localhost:\${port}\`; +})(); +const _publicBase = ( + process.env.PUBLICATION_URL || process.env.SITE_URL || "" +).replace(/\\/+$/, ""); + +function _toInternalUrl(url) { + if (!_publicBase || !url.startsWith(_publicBase)) return url; + return _internalBase + url.slice(_publicBase.length); +} + +export const endpoint = { + /** + * Micropub query + * @param {string} url - URL + * @param {string} accessToken - Access token + * @returns {Promise} Response data + */ + async get(url, accessToken) { + const fetchUrl = _toInternalUrl(url); + let endpointResponse; + try { + endpointResponse = await fetch(fetchUrl, { + headers: { + accept: "application/json", + authorization: \`Bearer \${accessToken}\`, + }, + }); } catch (fetchError) { const cause = fetchError.cause || fetchError; - console.error("[endpoint-posts] fetch failed for POST %s — %s: %s", url, cause.code || cause.name, cause.message); - if (cause.cause) console.error("[endpoint-posts] nested cause: %s", cause.cause.message || cause.cause); + console.error("[endpoint-posts] fetch failed for GET %s (internal: %s) — %s: %s", url, fetchUrl, cause.code || cause.name, cause.message); + throw fetchError; + } + + if (!endpointResponse.ok) { + throw await IndiekitError.fromFetch(endpointResponse); + } + + const body = await endpointResponse.json(); + + return body; + }, + + /** + * Micropub action + * @param {string} url - URL + * @param {string} accessToken - Access token + * @param {object} [jsonBody] - JSON body + * @returns {Promise} Response data + */ + async post(url, accessToken, jsonBody = false) { + const fetchUrl = _toInternalUrl(url); + let endpointResponse; + try { + endpointResponse = await fetch(fetchUrl, { + method: "POST", + headers: { + accept: "application/json", + authorization: \`Bearer \${accessToken}\`, + ...(jsonBody && { "content-type": "application/json" }), + }, + ...(jsonBody && { body: JSON.stringify(jsonBody) }), + }); + } catch (fetchError) { + const cause = fetchError.cause || fetchError; + console.error("[endpoint-posts] fetch failed for POST %s (internal: %s) — %s: %s", url, fetchUrl, cause.code || cause.name, cause.message); throw fetchError; } @@ -65,52 +192,20 @@ const newPostEnd = ` }); }, };`; -const oldGet = ` async get(url, accessToken) { - const endpointResponse = await fetch(url, {`; - -const newGet = ` async get(url, accessToken) { - let endpointResponse; - try { - endpointResponse = await fetch(url, {`; - -const oldGetEnd = ` }); - - if (!endpointResponse.ok) { - throw await IndiekitError.fromFetch(endpointResponse); - } - - const body = await endpointResponse.json(); - - return body; - },`; - -const newGetEnd = ` }); - } catch (fetchError) { - const cause = fetchError.cause || fetchError; - console.error("[endpoint-posts] fetch failed for GET %s — %s: %s", url, cause.code || cause.name, cause.message); - if (cause.cause) console.error("[endpoint-posts] nested cause: %s", cause.cause.message || cause.cause); - throw fetchError; - } - - if (!endpointResponse.ok) { - throw await IndiekitError.fromFetch(endpointResponse); - } - - const body = await endpointResponse.json(); - - return body; - },`; - -let updated = source; -updated = updated.replace(oldPost, newPost); -updated = updated.replace(oldPostEnd, newPostEnd); -updated = updated.replace(oldGet, newGet); -updated = updated.replace(oldGetEnd, newGetEnd); - -if (!updated.includes(marker)) { - console.warn("[postinstall] Skipping endpoint-posts fetch-diagnostic patch: upstream format changed"); +// Try matching the original (unpatched) file first +if (cleanSource.includes(original.trim())) { + const updated = cleanSource.replace(original.trim(), patched.trim()); + await writeFile(filePath, updated, "utf8"); + console.log("[postinstall] Patched endpoint-posts: fetch URL rewrite + diagnostic logging"); process.exit(0); } -await writeFile(filePath, updated, "utf8"); -console.log("[postinstall] Patched endpoint-posts with fetch diagnostic logging"); +// If old diagnostic patch was applied, try matching that version +if (cleanSource.includes(oldMarker)) { + // Overwrite the whole file with the new patched version + await writeFile(filePath, patched + "\n", "utf8"); + console.log("[postinstall] Replaced old fetch-diagnostic patch with fetch-rewrite + diagnostic"); + process.exit(0); +} + +console.warn("[postinstall] Skipping endpoint-posts fetch-rewrite patch: upstream format changed"); diff --git a/scripts/patch-micropub-fetch-internal-url.mjs b/scripts/patch-micropub-fetch-internal-url.mjs new file mode 100644 index 00000000..7cdc52be --- /dev/null +++ b/scripts/patch-micropub-fetch-internal-url.mjs @@ -0,0 +1,113 @@ +/** + * Patch: rewrite micropub self-fetch URLs to localhost in endpoint-syndicate + * and endpoint-share. + * + * Same issue as endpoint-posts: behind a reverse proxy (nginx in a separate + * FreeBSD jail), Node can't reach its own public HTTPS URL because port 443 + * only exists on the nginx jail. + * + * Rewrites fetch(application.micropubEndpoint, ...) to use + * http://localhost: instead. + */ + +import { access, readFile, writeFile } from "node:fs/promises"; + +const marker = "// [patch] micropub-fetch-internal-url"; + +const helperBlock = `${marker} +const _mpInternalBase = (() => { + if (process.env.INTERNAL_FETCH_URL) return process.env.INTERNAL_FETCH_URL.replace(/\\/+$/, ""); + const port = process.env.PORT || "3000"; + return \`http://localhost:\${port}\`; +})(); +const _mpPublicBase = ( + process.env.PUBLICATION_URL || process.env.SITE_URL || "" +).replace(/\\/+$/, ""); +function _toInternalUrl(url) { + if (!_mpPublicBase || !url.startsWith(_mpPublicBase)) return url; + return _mpInternalBase + url.slice(_mpPublicBase.length); +} +`; + +const targets = [ + { + paths: [ + "node_modules/@indiekit/endpoint-syndicate/lib/controllers/syndicate.js", + ], + oldSnippet: ` const micropubResponse = await fetch(application.micropubEndpoint, {`, + newSnippet: ` const micropubResponse = await fetch(_toInternalUrl(application.micropubEndpoint), {`, + }, + { + paths: [ + "node_modules/@indiekit/endpoint-share/lib/controllers/share.js", + ], + oldSnippet: ` const micropubResponse = await fetch(application.micropubEndpoint, {`, + newSnippet: ` const micropubResponse = await fetch(_toInternalUrl(application.micropubEndpoint), {`, + }, +]; + +async function exists(filePath) { + try { + await access(filePath); + return true; + } catch { + return false; + } +} + +let totalPatched = 0; + +for (const target of targets) { + for (const filePath of target.paths) { + if (!(await exists(filePath))) continue; + + const source = await readFile(filePath, "utf8"); + + if (source.includes(marker)) { + continue; + } + + if (!source.includes(target.oldSnippet)) { + console.warn(`[postinstall] micropub-fetch-internal-url: snippet not found in ${filePath} — skipping`); + continue; + } + + // Insert helper block after the last import statement. + // Find the last "from" keyword followed by a string and semicolon, + // which marks the end of the last import. + const importEndPattern = /;\s*\n/g; + const allImportMatches = [...source.matchAll(/^import\s/gm)]; + if (allImportMatches.length === 0) { + console.warn(`[postinstall] micropub-fetch-internal-url: no imports found in ${filePath} — skipping`); + continue; + } + + // Find the semicolon+newline that ends the last import block + const lastImportStart = allImportMatches.at(-1).index; + const afterLastImport = source.slice(lastImportStart); + const fromMatch = afterLastImport.match(/from\s+["'][^"']+["']\s*;\s*\n/); + if (!fromMatch) { + console.warn(`[postinstall] micropub-fetch-internal-url: can't find end of last import in ${filePath} — skipping`); + continue; + } + + const insertAt = lastImportStart + fromMatch.index + fromMatch[0].length; + const beforeHelper = source.slice(0, insertAt); + const afterHelper = source.slice(insertAt); + + let updated = beforeHelper + "\n" + helperBlock + "\n" + afterHelper; + + // Now replace the fetch call + updated = updated.replace(target.oldSnippet, target.newSnippet); + + await writeFile(filePath, updated, "utf8"); + console.log(`[postinstall] Patched micropub-fetch-internal-url in ${filePath}`); + totalPatched++; + } +} + +if (totalPatched === 0) { + console.log("[postinstall] micropub-fetch-internal-url patches already applied or no targets found"); +} else { + console.log(`[postinstall] micropub-fetch-internal-url: patched ${totalPatched} file(s)`); +}