Normalize ActivityPub profile URLs to fix WebFinger invalid URL
This commit is contained in:
@@ -86,12 +86,14 @@
|
||||
- ActivityPub federation is enabled via `@rmdes/indiekit-endpoint-activitypub`.
|
||||
- Actor handle resolution order is: `AP_HANDLE`, then `ACTIVITYPUB_HANDLE`, then `GITHUB_USERNAME`, then publication hostname first label.
|
||||
- Actor profile seed values come from `AUTHOR_NAME`, `AUTHOR_BIO`, `AUTHOR_AVATAR`, and `SITE_DESCRIPTION`.
|
||||
- `AUTHOR_AVATAR` can be absolute (`https://...`) or slash-relative (`/images/avatar.jpg`); startup normalizes it to an absolute URL.
|
||||
- Optional ActivityPub variables:
|
||||
- `AP_ALSO_KNOWN_AS` (Mastodon migration alias URL)
|
||||
- `AP_LOG_LEVEL` (`debug|info|warning|error|fatal`, default `info`)
|
||||
- `AP_DEBUG` (`1` or `true` enables debug dashboard)
|
||||
- `AP_DEBUG_PASSWORD` (required when debug dashboard is enabled)
|
||||
- `REDIS_URL` (recommended for production delivery queue durability)
|
||||
- Startup preflight `scripts/preflight-activitypub-profile-urls.mjs` normalizes existing ActivityPub profile URL fields in MongoDB (`url`, `icon`, `image`, `alsoKnownAs`) so WebFinger/actor responses do not fail on invalid URL values.
|
||||
- The ActivityPub locale patch creates/repairs `locales/de.json` from `locales/en.json` so backend UI keys do not render as raw `activitypub.*` translation strings when `SITE_LOCALE=de`.
|
||||
- Quick verification commands:
|
||||
- `curl -s "https://blog.giersig.eu/.well-known/webfinger?resource=acct:<handle>@blog.giersig.eu" | jq .`
|
||||
@@ -104,10 +106,11 @@
|
||||
- `start.sh` is intentionally ignored by Git (`.gitignore`) so server secrets are not committed.
|
||||
- Use `start.example.sh` as the tracked template and keep real credentials in environment variables (or `.env` on the server).
|
||||
- Startup scripts parse `.env` with the `dotenv` parser (not shell `source`), so values containing spaces are handled safely.
|
||||
- Startup scripts run preflight + patch helpers before boot (`scripts/preflight-production-security.mjs`, `scripts/preflight-mongo-connection.mjs`, `scripts/patch-lightningcss.mjs`, `scripts/patch-endpoint-media-scope.mjs`, `scripts/patch-endpoint-media-sharp-runtime.mjs`, `scripts/patch-frontend-sharp-runtime.mjs`, `scripts/patch-endpoint-files-upload-route.mjs`, `scripts/patch-endpoint-files-upload-locales.mjs`, `scripts/patch-endpoint-activitypub-locales.mjs`, `scripts/patch-frontend-serviceworker-file.mjs`, `scripts/patch-conversations-collection-guards.mjs`, `scripts/patch-indiekit-routes-rate-limits.mjs`, `scripts/patch-indiekit-error-production-stack.mjs`, `scripts/patch-indieauth-devmode-guard.mjs`, `scripts/patch-listening-endpoint-runtime-guards.mjs`).
|
||||
- Startup scripts run preflight + patch helpers before boot (`scripts/preflight-production-security.mjs`, `scripts/preflight-mongo-connection.mjs`, `scripts/preflight-activitypub-profile-urls.mjs`, `scripts/patch-lightningcss.mjs`, `scripts/patch-endpoint-media-scope.mjs`, `scripts/patch-endpoint-media-sharp-runtime.mjs`, `scripts/patch-frontend-sharp-runtime.mjs`, `scripts/patch-endpoint-files-upload-route.mjs`, `scripts/patch-endpoint-files-upload-locales.mjs`, `scripts/patch-endpoint-activitypub-locales.mjs`, `scripts/patch-frontend-serviceworker-file.mjs`, `scripts/patch-conversations-collection-guards.mjs`, `scripts/patch-indiekit-routes-rate-limits.mjs`, `scripts/patch-indiekit-error-production-stack.mjs`, `scripts/patch-indieauth-devmode-guard.mjs`, `scripts/patch-listening-endpoint-runtime-guards.mjs`).
|
||||
- The production security preflight blocks startup on insecure auth/session configuration and catches empty-password bcrypt hashes.
|
||||
- One-time recovery mode is available with `INDIEKIT_ALLOW_PASSWORD_SETUP=1` to bootstrap/reset `PASSWORD_SECRET` when locked out. Remove this flag after setting a valid hash.
|
||||
- The media scope patch fixes a known upstream issue where file uploads can fail if the token scope is `create update delete` without explicit `media`.
|
||||
- The ActivityPub profile URL preflight repairs invalid URL fields in the `ap_profile` document (for example relative `icon` paths), preventing `/.well-known/webfinger` and actor responses from failing with `TypeError: Invalid URL`.
|
||||
- The media sharp runtime patch makes image transformation resilient on FreeBSD: if `sharp` cannot load, uploads continue without resize/rotation instead of crashing the server process.
|
||||
- The frontend sharp runtime patch makes icon generation non-fatal on FreeBSD when `sharp` cannot load, preventing startup crashes in asset controller imports.
|
||||
- The files upload route patch fixes browser multi-upload by posting to `/files/upload` (session-authenticated) instead of direct `/media` calls without bearer token.
|
||||
|
||||
@@ -37,7 +37,9 @@ const funkwhaleToken = process.env.FUNKWHALE_TOKEN;
|
||||
const lastfmApiKey = process.env.LASTFM_API_KEY;
|
||||
const lastfmUsername = process.env.LASTFM_USERNAME;
|
||||
const publicationBaseUrl = (
|
||||
process.env.PUBLICATION_URL || "https://blog.giersig.eu"
|
||||
process.env.PUBLICATION_URL ||
|
||||
process.env.SITE_URL ||
|
||||
"https://blog.giersig.eu"
|
||||
).replace(/\/+$/, "");
|
||||
const publicationHostname = (() => {
|
||||
try {
|
||||
@@ -51,7 +53,19 @@ const debugEnabled = process.env.INDIEKIT_DEBUG === "1" || nodeEnv !== "producti
|
||||
const siteName = process.env.SITE_NAME || "Indiekit";
|
||||
const authorName = process.env.AUTHOR_NAME || "";
|
||||
const authorBio = process.env.AUTHOR_BIO || "";
|
||||
const authorAvatar = process.env.AUTHOR_AVATAR || "";
|
||||
const authorAvatar = (() => {
|
||||
const avatar = (process.env.AUTHOR_AVATAR || "").trim();
|
||||
|
||||
if (!avatar) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(avatar, publicationBaseUrl).href;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
})();
|
||||
const activityPubHandle = (
|
||||
process.env.AP_HANDLE ||
|
||||
process.env.ACTIVITYPUB_HANDLE ||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"postinstall": "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-frontend-serviceworker-file.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",
|
||||
"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/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-frontend-serviceworker-file.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 node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.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-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-frontend-serviceworker-file.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 node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
175
scripts/preflight-activitypub-profile-urls.mjs
Normal file
175
scripts/preflight-activitypub-profile-urls.mjs
Normal file
@@ -0,0 +1,175 @@
|
||||
import { MongoClient } from "mongodb";
|
||||
|
||||
import config from "../indiekit.config.mjs";
|
||||
|
||||
const strictMode = process.env.REQUIRE_MONGO !== "0";
|
||||
const mongodbUrl = config.application?.mongodbUrl;
|
||||
const publicationBaseUrl = (() => {
|
||||
const candidate =
|
||||
config.publication?.me ||
|
||||
process.env.PUBLICATION_URL ||
|
||||
process.env.SITE_URL ||
|
||||
"https://blog.giersig.eu";
|
||||
|
||||
try {
|
||||
return new URL(candidate).href;
|
||||
} catch {
|
||||
return "https://blog.giersig.eu/";
|
||||
}
|
||||
})();
|
||||
|
||||
function toHttpUrl(value, { baseUrl, allowRelative = false } = {}) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const absolute = new URL(trimmed);
|
||||
|
||||
if (absolute.protocol === "http:" || absolute.protocol === "https:") {
|
||||
return absolute.href;
|
||||
}
|
||||
|
||||
return "";
|
||||
} catch {
|
||||
if (!allowRelative) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const resolved = new URL(trimmed, baseUrl);
|
||||
|
||||
if (resolved.protocol === "http:" || resolved.protocol === "https:") {
|
||||
return resolved.href;
|
||||
}
|
||||
|
||||
return "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readAliases(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.filter((entry) => typeof entry === "string")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? [trimmed] : [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeAliases(value, baseUrl) {
|
||||
const aliases = [];
|
||||
|
||||
for (const entry of readAliases(value)) {
|
||||
// Only resolve slash-relative aliases. Non-URL handles like @user@host are dropped.
|
||||
const normalized = toHttpUrl(entry, {
|
||||
baseUrl,
|
||||
allowRelative: entry.startsWith("/"),
|
||||
});
|
||||
|
||||
if (normalized && !aliases.includes(normalized)) {
|
||||
aliases.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
if (!mongodbUrl) {
|
||||
console.warn(
|
||||
"[preflight] ActivityPub profile URL sync skipped: MongoDB URL is not configured.",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const client = new MongoClient(mongodbUrl, { connectTimeoutMS: 5000 });
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
const apProfile = client.db().collection("ap_profile");
|
||||
const profile = await apProfile.findOne({});
|
||||
|
||||
if (!profile) {
|
||||
console.log(
|
||||
"[preflight] ActivityPub profile URL sync skipped: no profile document found.",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const updates = {};
|
||||
const normalizedProfileUrl =
|
||||
toHttpUrl(profile.url, { baseUrl: publicationBaseUrl, allowRelative: true }) ||
|
||||
publicationBaseUrl;
|
||||
|
||||
if ((profile.url || "") !== normalizedProfileUrl) {
|
||||
updates.url = normalizedProfileUrl;
|
||||
}
|
||||
|
||||
const normalizedIcon = toHttpUrl(profile.icon, {
|
||||
baseUrl: publicationBaseUrl,
|
||||
allowRelative: true,
|
||||
});
|
||||
|
||||
if ((profile.icon || "") !== normalizedIcon) {
|
||||
updates.icon = normalizedIcon;
|
||||
}
|
||||
|
||||
const normalizedImage = toHttpUrl(profile.image, {
|
||||
baseUrl: publicationBaseUrl,
|
||||
allowRelative: true,
|
||||
});
|
||||
|
||||
if ((profile.image || "") !== normalizedImage) {
|
||||
updates.image = normalizedImage;
|
||||
}
|
||||
|
||||
const originalAliases = readAliases(profile.alsoKnownAs);
|
||||
const normalizedAliases = normalizeAliases(profile.alsoKnownAs, publicationBaseUrl);
|
||||
|
||||
if (JSON.stringify(originalAliases) !== JSON.stringify(normalizedAliases)) {
|
||||
updates.alsoKnownAs = normalizedAliases;
|
||||
}
|
||||
|
||||
const fields = Object.keys(updates);
|
||||
|
||||
if (fields.length === 0) {
|
||||
console.log("[preflight] ActivityPub profile URL fields already valid");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await apProfile.updateOne({ _id: profile._id }, { $set: updates });
|
||||
console.log(
|
||||
`[preflight] ActivityPub profile URL fields normalized: ${fields.join(", ")}`,
|
||||
);
|
||||
} catch (error) {
|
||||
const message = `[preflight] ActivityPub profile URL sync failed: ${error.message}`;
|
||||
|
||||
if (strictMode) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.warn(`${message} Continuing because strict mode is disabled.`);
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,9 @@ unset DEBUG
|
||||
# Verify MongoDB credentials/connectivity before launching server.
|
||||
/usr/local/bin/node scripts/preflight-mongo-connection.mjs
|
||||
|
||||
# Normalize ActivityPub profile URL fields (icon/image/aliases) in MongoDB.
|
||||
/usr/local/bin/node scripts/preflight-activitypub-profile-urls.mjs
|
||||
|
||||
# Ensure runtime dependency patches are applied even if node_modules already exists.
|
||||
/usr/local/bin/node scripts/patch-lightningcss.mjs
|
||||
/usr/local/bin/node scripts/patch-endpoint-media-scope.mjs
|
||||
|
||||
Reference in New Issue
Block a user