Remote servers (Mastodon, etc.) require explicit audience addressing
to display a post. Without to/cc, the Note was silently discarded.
- to: as:Public (visible to everyone)
- cc: followers collection
- Upgrade @fedify/fedify, @fedify/redis to ^2.0.0
- Add @fedify/debugger ^2.0.0 for live federation traffic dashboard
- Move all vocab type imports to @fedify/fedify/vocab (13 files)
- Move crypto imports (exportJwk, importJwk, generateCryptoKeyPair) to @fedify/fedify/sig
- Replace removed importSpki() with local Web Crypto API helper
- Add KvStore.list() async generator required by Fedify 2.0
- Add setOutboxPermanentFailureHandler for delivery failure logging
- Add debugDashboard/debugPassword config options
- Skip manual LogTape configure when debugger auto-configures it
- Fix Express-Fedify bridge to reconstruct body from req.body when
Express body parser has already consumed the stream (fixes debug
dashboard login TypeError)
- Add response.bodyUsed safety check in sendFedifyResponse
- Remove @fedify/express dependency (custom bridge handles sub-path mounting)
Replace the browser redirect on /activitypub/users/:handle with a
standalone HTML profile page showing avatar, bio, profile fields,
stats (posts/following/followers/joined), follow-me prompt with
copy button, pinned posts, and recent posts. Supports light/dark
mode via prefers-color-scheme. ActivityPub clients still get JSON-LD
from Fedify before this route is reached.
broadcastActorUpdate() now fetches followers from MongoDB, deduplicates
by shared inbox, and delivers in batches of 25 with 5s delays to prevent
thundering herd (hundreds of 499s from simultaneous re-fetches).
Browser GET on /users/:handle now redirects to homepage instead of 404.
ctx.getActor() only exists on RequestContext (inside HTTP handlers), not
on the base Context returned by createContext(). Extracted actor-building
logic into shared buildPersonActor() helper used by both the dispatcher
and broadcastActorUpdate(). Profile link attachments now propagate to
remote instances via Update(Person) activity.
Reader now resolves ActivityPub links internally instead of navigating
to external instances. Actor links open the profile view, post links
open a new post detail view with thread context (parent chain + replies).
External links in post content get rich preview cards (title, description,
image, favicon) fetched via unfurl.js at ingest time with fire-and-forget
async processing and concurrency limiting.
New files: post-detail controller, og-unfurl module, lookup-cache,
link preview template/CSS, client-side link interception JS.
Includes SSRF protection for OG fetching and GoToSocial URL support.
- Fix Unknown authors by adding multi-strategy fallback chain in
extractObjectData (getAttributedTo → actorFallback → attributionIds)
- Fix empty boosts from Lemmy/PieFed by checking content before storing
- Fix @mention/hashtag styling to stay inline instead of breaking layout
- Fix compose reply to show sanitized HTML blockquote instead of raw text
- Add default-checked syndication targets for AP and Bluesky
- Use authenticated document loader for all lookupObject calls
(fixes 401 errors on servers requiring Authorized Fetch)
- Fix like handler 404 by using canonical AP uid for interactions
instead of display URLs; add data-item-uid to card template
- Fix profile bio showing Nunjucks macro source code by renaming
summary→bio to avoid collision with Indiekit's summary macro
- Fix Fedify API misuse in timeline-store.js: use instanceof Article
(not string comparison), replyTargetId (not inReplyTo), getTags()
and getAttachments() async methods (not sync property access)
- Fix inbox-listeners.js: use replyTargetId instead of non-existent
getInReplyTo(), use instanceof Article for Update handler
- Add error logging to interaction catch blocks
express.urlencoded({ extended: true }) uses qs which strips [] from
field names, so link_name[] arrives as request.body.link_name — not
request.body["link_name[]"]. The old lookup always got undefined,
producing an empty attachments array that overwrote existing links.
The attributionIds fallback was matching "users" from /users/NatalieDavis
instead of the actual username. Now handles /@name, /users/name, and
/ap/users/id patterns correctly.
- Add actorFallback option to extractObjectData() so the activity's
actor is used when object.getAttributedTo() fails (Authorized Fetch,
unreachable servers). Falls back to attributionIds for URL-based info.
- Pass create.getActor() as actorFallback in Create inbox listener.
- Skip storing boosts with no content (Lemmy/PieFed activity IDs).
- Add template guard to hide empty cards already in the database.
- Style @mention and hashtag links distinctly from prose content.
- Handle Mastodon's invisible/ellipsis URL span classes.
Replace all nonexistent CSS variable references with Indiekit's actual
custom properties. This enables automatic dark mode support (variables
swap via prefers-color-scheme) and visual consistency with the rest of
the admin UI.
Key changes:
- Map --color-text → --color-on-background, --color-text-muted →
--color-on-offset, --border-radius → --border-radius-small, etc.
- Add post-type differentiation via colored left borders: purple for
notes, green for articles, yellow for boosts, primary for replies
- Replace hardcoded hex colors (#e11d48, #16a34a) with Indiekit's
palette variables (--color-red45, --color-green50, etc.)
- Use Indiekit's border-width tokens for consistent border sizing
- Add background/color to form inputs for dark mode compatibility
Nunjucks resolves template names across all registered plugin view
directories. Both @rmdes/indiekit-endpoint-microsub and this plugin
had views/layouts/reader.njk, causing the microsub layout to be
loaded instead — which meant Alpine.js, reader CSS, and all timeline
content were missing from the rendered page.
- Return multiple navigation items (ActivityPub, Reader, Notifications, Moderation)
so all AP sub-pages are accessible from the sidebar
- Fix Alpine.js not loading: `{% block head %}` was silently discarded because
the parent template chain has no such block — moved script/css into content block
- Pin Alpine.js to exact version 3.14.9 to prevent CDN resolution issues
- Add fallback avatar (first letter) when author photo is missing
- Guard empty author URLs to prevent broken links
- Fix Temporal.Instant TypeError: use String() instead of new Date() for
Fedify published timestamps in inbox-listeners and timeline-store
- Link author names to remote profile view instead of raw AP URLs
- Bump to 1.1.3
- Add broadcastActorUpdate() method that sends Update(Person) to all
followers so remote servers re-fetch the actor object
- Profile, featured pin/unpin, and featured tags add/remove controllers
now trigger the broadcast after changes
- Wrap URL attachment values in <a rel="me"> HTML for Mastodon rel=me
verification; plain text values pass through unchanged
- Bump version to 1.1.1
Add a dedicated fediverse reader view with:
- Timeline view showing posts from followed accounts with threading,
content warnings, boosts, and media display
- Compose form with dual-path posting (quick AP reply + Micropub blog post)
- Native AP interactions (like, boost, reply, follow/unfollow)
- Notifications view for likes, boosts, follows, mentions, replies
- Moderation tools (mute/block actors, keyword filters)
- Remote actor profile pages with follow state
- Automatic timeline cleanup with configurable retention
- CSRF protection, XSS prevention, input validation throughout
Removes Microsub bridge dependency — AP content now lives in its own
MongoDB collections (ap_timeline, ap_notifications, ap_interactions,
ap_muted, ap_blocked).
Bumps version to 1.1.0.
- Actor type radio buttons (Person/Service/Organization) in Profile page,
stored in ap_profile and read by federation-setup actor dispatcher
- Profile links (attachments) section with add/remove for rel="me"
verification links, rendered as PropertyValue on the ActivityPub actor
- New locale strings for all new UI elements
Express 5 removed the "back" magic keyword from response.redirect().
It was treated as a literal URL, causing 404s at /admin/featured/back
and /admin/tags/back. Now redirects to the correct parent pages.
Inbox handlers used await activity.getObject() which HTTP-fetches remote
objects. This fails when remote servers have Authorized Fetch enabled or
are unavailable, causing Fedify to retry ~10 times per activity.
Replaced with .objectId/.actorId accessors (zero network requests) for
Like, Announce, Undo, and Delete handlers. Wrapped remaining getObject()
and getActor() calls in try-catch with fallback to ID accessors.
Also adds Pinned Posts and Featured Tags cards to the admin dashboard.
Implement all missing Fedify features for full ActivityPub compliance:
- Liked, Featured, Featured Tags collection dispatchers with admin UIs
- Object dispatcher for Note/Article dereferencing at AP URIs
- Instance actor (Application type) for domain-level federation
- Handle aliases (.mapAlias) for profile URL and /@handle resolution
- Configurable actor type (Person/Service/Organization/Group)
- Dynamic NodeInfo version from @indiekit/indiekit package.json
- Context data propagation (handle + publication URL)
- ParallelMessageQueue wrapping RedisMessageQueue (5 workers)
- Collection sync (FEP-8fcf) and ordering keys on sendActivity
- Permanent failure handler stub (deferred to Fedify 2.0)
- Profile attachments (PropertyValue) and alsoKnownAs support
- Strip invalid "type":"as:Endpoints" from actor JSON (Fedify #576)
- Fix .mapAlias() return type ({identifier} not bare string)
- Remove .authorize() predicate (causes 401 loops without auth doc loader)
- Narrow content negotiation router to /nodeinfo/ only
22/22 compliance tests pass (Grade A+). Version 1.0.26.
- Persist Ed25519 key pair to ap_keys collection via exportJwk/importJwk
instead of regenerating on every request (fixes OIP verification failures)
- Use assertionMethods (plural array) per Fedify spec
- Add @fedify/redis + ioredis for persistent message queue that survives
process restarts (falls back to InProcessMessageQueue when no Redis)
- Add Reject inbox listener to mark rejected Follow requests
- Add performance indexes on ap_followers, ap_following, ap_activities
- Wire storeRawActivities flag through to activity logging
- Bump version to 1.0.21
Replies syndicated via ActivityPub were only sent to followers.
Remote servers (e.g. Mastodon) never received the Create(Note) activity,
so replies didn't appear under the original post.
Changes:
- Resolve the reply-to post author via ctx.lookupObject() + getAttributedTo()
- Include the original author in CC addressing (ccs) on the Note
- Add a Mention tag for the original author
- Deliver the activity to the author's inbox via a second sendActivity() call
- Log reply delivery with targetUrl for debugging
Also includes: following list badge fix from refollow work, version bump to 1.0.20
Three issues fixed:
1. Progress bar invisible: used --color-accent (doesn't exist in
Indiekit theme). Changed to --color-primary.
2. Pause/resume buttons non-functional: the /admin/refollow/status
GET endpoint was intercepted by Fedify middleware (content
negotiation routes) returning 404 before Express saw it. Added
/admin path skip to content negotiation middleware. Also made
buttons toggle dynamically via Alpine.js x-show instead of
server-rendered {% if %}.
3. Status badge static: replaced Nunjucks badge macro with Alpine.js
x-text bound to a computed statusLabel property.
The AP plugin auto-created the Fediverse channel with userId: null,
but the Microsub reader UI filters channels by userId: "default".
The channel existed in MongoDB but was invisible to users.
getApChannelId() now:
- Auto-creates with userId: "default" if no channel exists
- Fixes existing channels missing userId (from earlier versions)
- Uses proper field pattern matching Microsub plugin conventions
Fedify resolves accept.getObject() to a Person (the Follow target actor)
rather than the Follow activity itself. The instanceof Follow check
rejected every Accept. Removed inner object inspection entirely — instead
match directly against ap_following where source is refollow:sent. If we
have a pending follow for this actor, any Accept from them confirms it.
Two issues prevented Accept activities from transitioning ap_following
docs from refollow:sent to federation:
1. accept.getObject() often returns null because remote servers reference
our outgoing Follow by URL, which Fedify can't resolve back. The strict
instanceof Follow check caused early return on every Accept. Now we
proceed to the MongoDB match if getObject() returns null or throws.
2. Batch processor sent Follow to entry.actorUrl but never updated the
stored URL to the canonical form after resolving the remote actor.
Now updates actorUrl to remoteActor.id.href so Accept handler matches.
After following remote actors, their servers deliver all activities
to our inbox — including likes/boosts of other people's posts. Only
log Likes and Announces where the objectUrl starts with our
publication URL, filtering out unrelated activity.
When Accept(Follow) arrives, source transitions from refollow:sent
to federation. Without counting federated in the total, those
accounts drop out of both numerator and denominator, making the
progress bar stay flat or go backwards.
Some resolved Note objects from Create activities don't have
getInReplyTo as a function (Fedify stub/partial resolution).
Add typeof check and try-catch to prevent inbox processing crash.
After Mastodon migration, imported accounts exist only locally — no
Follow activities were sent. This adds a gradual background processor
that sends Follow activities to all source:"import" accounts so remote
servers start delivering Create activities to our inbox.
- New batch engine (lib/batch-refollow.js) processes 10 accounts per
batch with 3s between follows and 30s between batches
- Accept(Follow) inbox listener transitions source to "federation"
and cleans up tracking fields
- Admin API: pause, resume, and status JSON endpoints
- Dashboard progress bar with Alpine.js polling (10s interval)
- Following list badges for refollow:sent and refollow:failed states
- Restart recovery resets stale refollow:pending back to import
- 3 retries with 1-hour cooldown before permanent failure
1. mapHandle() — tells Fedify how to resolve WebFinger usernames to
actor identifiers, suppressing the "No actor handle mapper is set"
warning on every WebFinger lookup.
2. Ed25519 key pair — generated alongside the legacy RSA pair so Fedify
can create Object Integrity Proofs on outbound activities. RSA is
kept for HTTP Signatures backward compatibility.
Two critical fixes for ActivityPub federation:
1. Call federation.startQueue() — without this, ctx.sendActivity() enqueues
delivery tasks but the InProcessMessageQueue never processes them, so
activities are never actually POSTed to follower inboxes.
2. Add setSharedKeyDispatcher on the shared inbox — enables Fedify to make
signed/authenticated GET requests when verifying incoming HTTP Signatures.
Servers with authorized fetch (e.g. hachyderm.io) return 401 on unsigned
requests, which prevented Fedify from fetching sender public keys and
caused all incoming activities to be rejected.
Configure LogTape to route Fedify's internal logs (federation, vocab,
delivery) to console at info level. This makes activity delivery
attempts, HTTP signature issues, and queue processing visible in
container logs.
Notes and articles syndicated to ActivityPub now include a clickable
link back to the canonical post URL at the end of the content body.
This ensures fediverse clients display a visible permalink, since the
Note url property alone is not shown inline by most implementations.
- inbox-listeners.js: Store `targetUrl` (inReplyTo) and `content` (HTML)
on Reply activities for the conversations plugin AP adapter
- activitypub-followers.njk: Fix photo property name (`src` → `url`)
to match the card component's expected interface, fixing TypeError
crash on followers page when avatars are present
- Bump to v1.0.4
The contentNegotiationRoutes getter is mounted at root / and was passing
ALL requests through Fedify, including POST requests to admin routes.
fromExpressRequest() calls Readable.toWeb(req) which consumes the body
stream, causing "response body object should not be distributed or locked"
errors when admin controllers try to read req.body.
The v1.0.2 fix only protected routesPublic (mounted at /activitypub).
This fixes the actual culprit by skipping non-GET/HEAD methods in
contentNegotiationRoutes, since content negotiation and NodeInfo are
both GET-only concerns.
POST to /admin/migrate was going through Fedify's federation.fetch()
which consumed the already-parsed request body stream, causing
"response body object should not be distributed or locked" errors.
Admin routes (/admin/*) are UI routes handled by authenticated
Express handlers, not federation endpoints.
- Return 405 for GET on inbox endpoints instead of falling through
to Indiekit's auth middleware (which redirects to login)
- Add handlers for Update (refresh follower data), Block (remove
follower), Add and Remove (Mastodon pin/unpin — ignored)
- Bump to 1.0.1
Express's app-level body parser has a 100KB default limit that
runs before any route-level overrides. A 3K-line CSV at 113KB
exceeds this. Instead of sending raw CSV, the client now extracts
handles (first column only) and sends just the array — typically
under 90KB for 3000 accounts.
The app-level Express urlencoded parser (100KB limit) runs before
route-level middleware, so overriding the limit on the route doesn't
help. Solution: POST the CSV as JSON via fetch() to a dedicated
/admin/migrate/import endpoint with its own express.json({ limit: '5mb' }).
- Import button now shows "Importing..." while working
- Results appear inline without page reload
- Failed handles shown in a collapsible details element
- Import button disabled until a file is selected
- Alias form remains a regular POST (small payload, no issue)
- Add express.urlencoded({ limit: '5mb' }) to migration POST route
to handle large CSV files (default 100KB was too small)
- Add per-handle progress logging to console for monitoring imports
- Log failed handles with reasons (WebFinger failure, no AP link, etc.)
- Show failed handles in the UI result notification
- Use error notification type when all imports fail
Multipart form uploads fail because Indiekit has no multipart parsing
middleware. Instead, read the CSV file client-side with FileReader and
submit the text content as a hidden form field. Shows file name and
line count after selection for user confidence.
- Show current alias value on the page (persists across GET/POST)
- Pre-fill alias input with current value
- Add fieldset legend and per-item hints to import checkboxes
- Add intro paragraph explaining the migration flow
- Rewrite copy to be clearer and more reassuring
- Note irreversibility of step 3 explicitly