fix(journal): actor profile fields must serialize as a JSON array

Mastodon's PropertyValue parser requires the actor's attachment to be
a JSON *array* and silently ignores a bare object — and Fedify
compacts single-element arrays to bare objects, so the 🥾 trails.cool
field shipped in #464 never rendered (verified: Mastodon stored
fields = {} after a forced actor refresh on the soak instance).

Ship two fields so the array survives serialization: the 🥾 profile
link (rel=me) plus an Instance link — the latter is genuinely useful
for self-hosted instances anyway. Test now asserts the array shape
explicitly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-06-07 08:43:03 +02:00
parent b0bfed4d4a
commit d74ce32cd0
2 changed files with 20 additions and 6 deletions

View file

@ -95,12 +95,18 @@ describe("actor object", () => {
expect(actor.preferredUsername).toBe("bruno");
expect(actor.name).toBe("Bruno");
expect(actor.summary).toBe("Riding bikes");
// Profile-metadata field Mastodon renders ("this is a trails profile")
const attachments = Array.isArray(actor.attachment) ? actor.attachment : [actor.attachment];
const field = attachments.find((a: { type: string }) => a?.type === "PropertyValue");
expect(field?.name).toBe("🥾 trails.cool");
expect(field?.value).toContain('href="http://localhost:3000/users/bruno"');
expect(field?.value).toContain('rel="me"');
// Profile-metadata fields Mastodon renders ("this is a trails
// profile"). MUST serialize as a JSON array — Mastodon's parser
// silently ignores a bare attachment object, which is what Fedify
// compacts single-element arrays into (hence two fields).
expect(Array.isArray(actor.attachment)).toBe(true);
const fields = actor.attachment.filter((a: { type: string }) => a?.type === "PropertyValue");
expect(fields.length).toBeGreaterThanOrEqual(2);
const trails = fields.find((f: { name: string }) => f.name === "🥾 trails.cool");
expect(trails?.value).toContain('href="http://localhost:3000/users/bruno"');
expect(trails?.value).toContain('rel="me"');
const instance = fields.find((f: { name: string }) => f.name === "Instance");
expect(instance?.value).toContain("localhost:3000");
});
it("404s the actor for a private user", async () => {

View file

@ -180,11 +180,19 @@ function buildFederation(): Federation<void> {
// metadata table — a human-visible "this is a trails profile"
// marker. (The machine-readable marker is NodeInfo; this is
// flair.) Mastodon strips most HTML in values but keeps links.
// NOTE: Mastodon's parser requires `attachment` to be a JSON
// *array* and silently ignores a bare object — and Fedify
// compacts single-element arrays to bare objects. Keep at least
// two fields here so the array survives serialization.
attachments: [
new PropertyValue({
name: "🥾 trails.cool",
value: `<a href="${localActorIri(identifier)}" rel="me">${getOrigin().replace(/^https?:\/\//, "")}/users/${identifier}</a>`,
}),
new PropertyValue({
name: "Instance",
value: `<a href="${getOrigin()}">${getOrigin().replace(/^https?:\/\//, "")}</a>`,
}),
],
});
})