Files
indiekit-server/scripts/patch-endpoint-posts-fetch-diagnostic.mjs
Sven 733c00b1b3 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 <noreply@anthropic.com>
2026-03-16 20:24:46 +01:00

212 lines
6.7 KiB
JavaScript

/**
* 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:<PORT> 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-internal-rewrite";
async function exists(p) {
try {
await access(p);
return true;
} catch {
return false;
}
}
if (!(await exists(filePath))) {
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-rewrite patch already applied");
process.exit(0);
}
// 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 original = `import { IndiekitError } from "@indiekit/error";
export const endpoint = {
/**
* Micropub query
* @param {string} url - URL
* @param {string} accessToken - Access token
* @returns {Promise<object>} 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<object>} Response data
*/
async post(url, accessToken, jsonBody = false) {
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);
}
return endpointResponse.status === 204
? { success_description: endpointResponse.headers.get("location") }
: await endpointResponse.json();
},
};`;
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<object>} 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 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<object>} 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;
}
if (!endpointResponse.ok) {
throw await IndiekitError.fromFetch(endpointResponse);
}
return endpointResponse.status === 204
? { success_description: endpointResponse.headers.get("location") }
: await endpointResponse.json();
},
};`;
// 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);
}
// 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");