Commit Graph

186 Commits

Author SHA1 Message Date
Ricardo
09dc4ea3f5 chore: bump version to 3.9.0
Security audit remediation, architecture refactors, performance
improvements, and UI/UX fixes. Breaking: OAuth client secrets are
now hashed — existing apps must re-register.
2026-03-25 07:43:25 +01:00
Ricardo
12454749ad fix: comprehensive security, performance, and architecture audit fixes
27 issues fixed from multi-dimensional code review (4 Critical, 6 High, 11 Medium, 6 Low):

Security (Critical):
- Escape HTML in OAuth authorization page to prevent XSS (C1)
- Add CSRF protection to OAuth authorize flow (C2)
- Replace bypassable regex sanitizer with sanitize-html library (C3)
- Enforce OAuth scopes on all Mastodon API routes (C4)

Security (Medium/Low):
- Fix SSRF via DNS resolution before private IP check (M1)
- Add rate limiting to API, auth, and app registration endpoints (M2)
- Validate redirect_uri on POST /oauth/authorize (M4)
- Fix custom emoji URL injection with scheme validation + escaping (M5)
- Remove data: scheme from allowed image sources (L6)
- Add access token expiry (1hr) and refresh token rotation (90d) (M3)
- Hash client secrets before storage (L3)

Architecture:
- Extract batch-broadcast.js — shared delivery logic (H1a)
- Extract init-indexes.js — MongoDB index creation (H1b)
- Extract syndicator.js — syndication logic (H1c)
- Create federation-actions.js facade for controllers (M6)
- index.js reduced from 1810 to ~1169 lines (35%)

Performance:
- Cache moderation data with 30s TTL + write invalidation (H6)
- Increase inbox queue throughput to 10 items/sec (H5)
- Make account enrichment non-blocking with fire-and-forget (H4)
- Remove ephemeral getReplies/getLikes/getShares from ingest (M11)
- Fix LRU caches to use true LRU eviction (L1)
- Fix N+1 backfill queries with batch $in lookup (L2)

UI/UX:
- Split 3441-line reader.css into 15 feature-scoped files (H2)
- Extract inline Alpine.js interaction component (H3)
- Reduce sidebar navigation from 7 to 3 items (M7)
- Add ARIA live regions for dynamic content updates (M8)
- Extract shared CW/non-CW content partial (M9)
- Document form handling pattern convention (M10)
- Add accessible labels to functional emoji icons (L4)
- Convert profile editor to Alpine.js (L5)

Audit: documentation-central/audits/2026-03-24-activitypub-code-review.md
Plan: documentation-central/plans/2026-03-24-activitypub-audit-fixes.md
2026-03-25 07:41:20 +01:00
Ricardo
3ace60a1c8 fix: expose delete() and update() on ActivityPub syndicator object 2026-03-23 08:30:10 +01:00
Ricardo
ee0b0bb58a fix: broadcast Update activity (not Create) in broadcastPostUpdate
jf2ToAS2Activity() always returns a Create activity. For post edits,
Fediverse servers expect an Update wrapping the updated object, not a
second Create. Extract the Note/Article from the Create via getObject(),
then wrap it in new Update({ actor, object: note }) — matching the
existing broadcastActorUpdate() pattern exactly.
2026-03-23 08:25:24 +01:00
Ricardo
08200672ea refactor: replace res.json delete interception with syndicator delete() hook
Remove the fragile middleware in contentNegotiationRoutes that wrapped
res.json to detect successful Micropub delete responses. Replace it with
clean delete() and update() lifecycle methods on ActivityPubEndpoint that
are called directly by post-content.js via callSyndicatorHook.

Also adds broadcastPostUpdate() to send Update activities for edited posts,
mirroring the broadcastDelete() batch-delivery pattern.
2026-03-23 08:22:08 +01:00
Ricardo
c2920cafd8 fix: raw signed fetch fallback for author resolution
Servers like wafrn return AP JSON without @context, causing Fedify's
JSON-LD processor to reject the document. Strategy 1b in resolveAuthor
does a direct signed GET, extracts attributedTo/actor from plain JSON,
then resolves the actor via lookupWithSecurity.

Also: _loadRsaPrivateKey now imports with extractable=true (required
by Fedify's signRequest), and loadRsaKey is wired through to all
Mastodon API interaction helpers.
2026-03-23 07:56:34 +01:00
Ricardo
c71fd691a3 fix: direct follow workaround for tags.pub identity/v1 context rejection
tags.pub's activitypub-bot (activitystrea.ms parser) rejects any activity
body containing the https://w3id.org/identity/v1 JSON-LD context with
400 Invalid request body. Fedify 2.0 adds this context via LD Signatures
(RsaSignature2017) on all outbound activities.

Workaround: lib/direct-follow.js sends Follow/Undo(Follow) with a minimal
body (no LD Sig, no proof) using draft-cavage HTTP Signatures, scoped only
to tags.pub via DIRECT_FOLLOW_HOSTS set.

Also removes [federation-diag] inbox POST logging (no longer needed).

Upstream: https://github.com/social-web-foundation/tags.pub/issues/10
2026-03-22 19:40:12 +01:00
Ricardo
4495667ed9 fix: remove RSA Multikey from assertionMethod to fix tags.pub signature verification
The Fedify 2.0 migration added assertionMethods = keyPairs.map(k => k.multikey),
which places the RSA Multikey (id: #main-key) into assertionMethod alongside the
Ed25519 Multikey (id: #key-2).

This creates a keyId collision: the RSA CryptographicKey in publicKey and the RSA
Multikey in assertionMethod both use #main-key. Servers that traverse JSON-LD
properties alphabetically (assertionMethod before publicKey) find the Multikey
first — which lacks publicKeyPem — and return "public key not found".

Fix: filter assertionMethods to only Ed25519 keys (Object Integrity Proofs).
RSA keys already have their correct representation in publicKey (HTTP Signatures).
This matches Mastodon's behavior and is semantically correct per the two key systems.
2026-03-22 15:00:14 +01:00
Ricardo
9a0d6d208e fix: serve AP JSON for actor URLs without explicit text/html Accept header
Fedify's acceptsJsonLd() returns false for Accept: */* or no Accept header
because it only checks for explicit application/activity+json in the list.
Remote servers fetching actor URLs for HTTP Signature verification (e.g.
tags.pub) often omit Accept or use */*, getting HTML back instead of the
actor JSON and causing "public key not found" failures.

Add middleware to upgrade ambiguous Accept headers to application/activity+json
for GET requests to /users/:id paths. Explicit text/html requests (browsers)
are unaffected.

Also fix followActor() storing inbox: "" for actors where Fedify uses
remoteActor.inboxId?.href (not remoteActor.inbox?.id?.href). The inbox URL
is stored correctly now for all actor types.
2026-03-22 13:22:27 +01:00
Ricardo
89808f1dc5 chore: bump to 3.8.1 2026-03-22 00:26:44 +01:00
Ricardo
f69776b183 fix: isTagFollowed false positive for global-only follows; # stripping in getTagsPubActorUrl
- isTagFollowed() now checks doc?.followedAt instead of !!doc, so it
  correctly returns false for global-only follows (document exists but
  no local followedAt)
- getTagsPubActorUrl() strips leading # so URLs like ?tag=%23indieweb
  don't produce invalid https://tags.pub/user/#indieweb actor URLs
- Remove stale "Task 5" plan reference comment in tag timeline template
2026-03-22 00:25:29 +01:00
Ricardo
944917b3f0 feat: tags.pub global hashtag discovery integration (v3.8.0)
- Add setGlobalFollow/removeGlobalFollow/getFollowedTagsWithState to
  followed-tags storage; unfollowTag now preserves global follow state
- Add followTagGloballyController/unfollowTagGloballyController that
  send AP Follow/Undo via Fedify to tags.pub actor URLs
- Register POST /admin/reader/follow-tag-global and unfollow-tag-global
  routes with plugin reference for Fedify access
- Tag timeline controller passes isGloballyFollowed + error query param
- Tag timeline template adds global follow/unfollow buttons with globe
  indicator and inline error display
- Wire GET /api/v1/followed_tags to return real data with globalFollow state
- Add i18n keys: followGlobally, unfollowGlobally, globallyFollowing,
  globalFollowError
2026-03-22 00:22:47 +01:00
Ricardo
0d8b2d0f11 docs: update CLAUDE.md and README.md with Mastodon Client API layer
CLAUDE.md:
- Architecture: add full lib/mastodon/ tree (entities, helpers, middleware, routes)
- Data flow: add Mastodon API path (client → /api/v1/* → ap_timeline + Fedify)
- Collections: add ap_oauth_apps, ap_oauth_tokens, ap_markers; fix ap_blocked_servers field name
- Gotchas #34-35: Mastodon API architecture decisions (pagination, own-post detection,
  account enrichment, OAuth native app redirect, token storage, route ordering,
  unsigned fallback, backfill, content processing)
- Route table: add all Mastodon Client API endpoints

README.md:
- Updated description to mention Mastodon Client API compatibility
- Added full Mastodon Client API feature section
- Added moderation overview to Admin UI features
2026-03-21 20:50:36 +01:00
Ricardo
283c7ba9d0 feat: Mastodon Client API layer for Phanpy/Elk/Moshidon/Fedilab compatibility
Implement the Mastodon Client REST API (/api/v1/*, /api/v2/*) and OAuth2
server within the ActivityPub plugin, enabling Mastodon-compatible clients
to connect to our Fedify-based server.

Core features:
- OAuth2 with PKCE (S256) — app registration, authorization, token exchange
- HTML+JS redirect for native app custom URI schemes (Android WebView fix)
- Instance info + nodeinfo for client discovery
- Account lookup/search with remote WebFinger resolution via Fedify
- Home/public/hashtag timelines with published-date cursor pagination
- Status creation via Micropub pipeline with URL linkification and @mention extraction
- Favourite, boost, bookmark interactions with AP federation
- Notifications with type filtering and pagination
- Thread context (ancestors + descendants)
- Remote profile resolution with follower/following/post counts from AP collections
- Account stats enrichment in timeline responses (for Phanpy)
- In-memory account stats cache (500 entries, 1h TTL)
- Domain blocks API and moderation data in federation admin page
- Centralized unsigned fallback in lookupWithSecurity for servers rejecting signed GETs
- Timeline backfill from posts collection with content synthesis for bookmarks/likes/reposts
- 25+ stub endpoints preventing client errors on unimplemented features

Tested with: Phanpy (web), Elk (web), Moshidon (Android), Fedilab (Android)
2026-03-21 20:44:56 +01:00
Ricardo
c30657ef71 feat: surface moderation data in federation admin + Mastodon API
1. Federation admin page (/admin/federation): new Moderation section
   showing blocked servers (with hostnames), blocked accounts, and
   muted accounts/keywords

2. GET /api/v1/domain_blocks: returns actual blocked server hostnames
   from ap_blocked_servers (was stub returning [])

3. Relationship responses: domain_blocking field now checks if the
   account's domain matches a blocked server hostname (was always false)
2026-03-21 20:03:19 +01:00
Ricardo
76e9ba0b35 fix: centralize unsigned fallback in lookupWithSecurity
Some servers (e.g., tags.pub) return 400 for signed GET requests.
Previously only followActor had an unsigned fallback — all other
callers (resolve, unfollowActor, profile viewer, messages, post
detail, OG unfurl) would silently fail.

Fix: moved the fallback logic into lookupWithSecurity itself. When
an authenticated documentLoader is provided and the lookup fails,
it automatically retries without the loader (unsigned GET). This
fixes ALL AP resolution paths in one place — resolve, follow,
unfollow, profile viewing, message sending, quote fetching.

Removed individual fallbacks in followActor and resolve controller
since the central helper now handles it.
2026-03-21 19:16:05 +01:00
Ricardo
94c4546234 feat: linkify URLs and extract @mentions in status creation
Mastodon clients send plain text — the server must convert bare URLs
and @user@domain mentions into HTML links. Previously, URLs appeared
as plain text and mentions were not stored as mention objects.

- Bare URLs (http/https) are wrapped in <a> tags
- @user@domain patterns are converted to profile links with h-card markup
- Mentions are extracted into the mentions[] array with name and URL
- Only processes content that doesn't already contain <a> tags
  (avoids double-linkifying Micropub-rendered content)
2026-03-21 19:01:05 +01:00
Ricardo
cad9829cd7 fix: fallback to unsigned lookup when authenticated fetch fails in followActor
Some servers (e.g., tags.pub relay) reject or mishandle HTTP-signed GET
requests during actor resolution. The authenticated document loader is
tried first (required by Authorized Fetch servers like hachyderm.io),
then falls back to unsigned fetch if it returns null.

Same pattern should apply to unfollowActor.
2026-03-21 18:06:14 +01:00
Ricardo
ccb9cc99a2 fix: follow/unfollow fails for remotely resolved profiles
POST /accounts/:id/follow returned 404 for actors resolved via Fedify
(like @_followback@tags.pub) because resolveActorUrl only checked local
data (followers/following/timeline). These actors aren't in local
collections — they were resolved on-demand via WebFinger.

Fix: add reverse lookup map (accountId hash → actorUrl) to the account
cache. When resolveRemoteAccount resolves a profile, the hash-to-URL
mapping is stored alongside the stats. resolveActorUrl checks this
cache before scanning local collections.
2026-03-21 17:50:48 +01:00
Ricardo
30eff8e6c7 fix: status lookup fails due to published date format mismatch
findTimelineItemById decoded the cursor (ms-since-epoch) back to an ISO
date via toISOString() which produces "2026-03-21T15:33:50.000Z". But
the stored published dates lack the .000Z milliseconds suffix — they're
"2026-03-21T15:33:50Z". The exact string match failed for every single
status, breaking /statuses/:id, /statuses/:id/context, and all
interaction endpoints (favourite, boost, bookmark, delete).

Fix: try both formats — with .000Z first, then without.
2026-03-21 16:45:58 +01:00
Ricardo
35ed4a333e feat: enrich embedded account stats in timeline responses
Phanpy never calls /accounts/:id for timeline authors — it trusts the
embedded account object in each status. These showed 0 counts because
timeline author data doesn't include follower stats.

Fix: after serializing statuses, batch-resolve unique authors that have
0 counts via Fedify AP collection fetch (5 concurrent). Results are
cached (1h TTL) so subsequent page loads are instant.

Applied to all three timeline endpoints (home, public, hashtag).
2026-03-21 16:05:32 +01:00
Ricardo
fb11a517c0 chore: bump version to 3.6.8 2026-03-21 12:46:13 +01:00
Ricardo
af4d9f7ddf i18n: add translations for all 14 supported locales
Add de, es, es-419, fr, hi, id, it, nl, pl, pt, pt-BR, sr, sv,
zh-Hans-CN locale files (300 keys each).
2026-03-21 12:46:13 +01:00
Ricardo
3787be4c69 feat: cache remote account stats for embedded status accounts
Phanpy never calls /accounts/:id for timeline authors — it uses the
embedded account object from the status response. These had 0 counts
because the timeline author data doesn't include follower stats.

Fix: in-memory LRU cache (500 entries, 1h TTL) stores account stats
from remote resolutions. serializeAccount() reads from cache when
the actor has 0 counts, enriching embedded accounts with real data.

Cache is populated by resolveRemoteAccount() (lookup, search, and
/accounts/:id calls). Once a profile has been viewed once, all
subsequent status embeds for that author show real counts.
2026-03-21 12:38:27 +01:00
Ricardo
f9b8baec42 fix: route ordering + remote resolution for account profiles
Two bugs causing profile counts to show 0 in Phanpy:

1. Route ordering: /accounts/relationships and /accounts/familiar_followers
   were defined AFTER /accounts/:id. Express matched "relationships" as
   the :id parameter, returning 404. Moved them before the :id catch-all.

2. /accounts/:id only used local data (followers/following/timeline) which
   has no follower counts. Now tries remote actor resolution via Fedify
   to get real counts from AP collection totalItems.
2026-03-21 12:18:38 +01:00
Ricardo
bc72bf1e02 feat: populate remote profile counts, fields, and join date
Extract followers/following/statuses counts from AP collection
totalItems, profile fields from actor attachments, and published
date from the actor document. Previously showed 0/0/0 and today's
date for all remote profiles.
2026-03-21 12:06:49 +01:00
Ricardo
9f1287073b feat: resolve remote profiles via WebFinger in Mastodon API
Account lookup (/api/v1/accounts/lookup) and search (/api/v2/search)
now resolve remote actors via Fedify's ctx.lookupObject() when not
found locally. Previously only checked ap_followers — missed accounts
we follow, timeline authors, and any remote actor.

Lookup chain: local profile → followers → following → timeline authors
→ remote WebFinger+actor fetch (Fedify)

Search uses remote resolution when resolve=true and query contains @.
2026-03-21 11:49:12 +01:00
Ricardo
01edd6e92e fix: improve timeline content for own posts (4 issues)
1. Empty content on bookmarks/likes/reposts: synthesize content from
   the interaction target URL (bookmark-of, like-of, repost-of) when
   the post has no body text

2. Hashtags not extracted: parse #hashtag patterns from content text
   and merge with explicit categories. Applies to both backfill
   (startup) and POST /api/v1/statuses (runtime)

3. Hashtag links rewritten: /categories/tag/ links (site-internal)
   are rewritten to /tags/tag (Mastodon convention) in the HTML
   content stored in ap_timeline

4. Relative media URLs resolved: photo/video/audio URLs like
   media/photos/... are resolved to absolute URLs using the site URL
2026-03-21 10:34:11 +01:00
Ricardo
2a4ac75c77 fix: use HTML+JS redirect for native app OAuth callbacks
Android Chrome Custom Tabs block 302 redirects to custom URI schemes
(fedilab://, moshidon-android-auth://) for security. The server sends
the redirect correctly but the WebView silently ignores it — "nothing
happens" when the user taps Authorize.

Fix: detect non-HTTP redirect URIs and render an HTML page with both
a JavaScript window.location redirect and a meta refresh fallback.
Client-side navigation to custom schemes is allowed by WebViews.

HTTP(S) redirect URIs (Phanpy, Elk) still use standard 302.
2026-03-21 09:42:31 +01:00
Ricardo
41c43be4cb fix: rename variable to avoid 'published' redeclaration (SyntaxError) 2026-03-20 20:36:51 +01:00
Ricardo
c0d4b77b94 fix: sort Mastodon API timeline by published date instead of ObjectId
The Mastodon API timeline sorted by MongoDB _id (insertion order), not
by published date. This caused chronological jumps — backfilled or
syndicated posts got ObjectIds at import time, interleaving them
incorrectly with federation-received posts.

Changes:
- Pagination cursors now use published date (encoded as ms-since-epoch)
  instead of ObjectId. Mastodon clients pass these as opaque max_id/
  min_id/since_id values and they sort correctly.
- Status and notification IDs are now encodeCursor(published) so the
  cursor round-trips through client pagination.
- Status lookups (GET/DELETE /statuses/:id, context, interactions) use
  findTimelineItemById() which tries published-based lookup first, then
  falls back to ObjectId for backwards compatibility.
- Link pagination headers emit published-based cursors.

This matches the native reader's sort (storage/timeline.js) which has
always sorted by published: -1.
2026-03-20 18:05:45 +01:00
Ricardo
a8947b205f fix: omit null fields instead of setting them in OAuth token documents
MongoDB sparse indexes skip documents where the indexed field is ABSENT,
but still enforce uniqueness on explicit null values. The auth code insert
set accessToken:null and the client_credentials insert set code:null,
causing E11000 duplicate key errors on the second authorization attempt.

Fix: omit accessToken/code entirely from inserts where they don't apply.
The field gets added later during token exchange ($set in updateOne).
2026-03-20 17:25:25 +01:00
Ricardo
f55cfbfcd2 fix: use existing default-avatar.svg instead of missing placeholder-avatar.png
The fallback avatar URL pointed to /placeholder-avatar.png which doesn't
exist (404). Changed to /images/default-avatar.svg which exists in the
Eleventy theme and is served by the nginx image caching location with
CORS headers — fixing cross-origin errors in Phanpy/Elk.
2026-03-20 15:30:50 +01:00
Ricardo
d42b2fce9a chore: bump version to 3.5.7 2026-03-20 14:03:19 +01:00
Ricardo
0cde298b46 fix: detect own posts in Mastodon API status serialization
Own posts in ap_timeline have author.url set to the publication URL
(site root like "https://rmendes.net/") with no /@handle or /users/handle
pattern. extractUsername("/") returns "" which falls back to "unknown".

Fix: set module-level local identity (publicationUrl + handle) at plugin
init via setLocalIdentity(). serializeStatus() compares item.author.url
against the publication URL and passes isLocal:true + handle to
serializeAccount() when they match.

This is zero-cost for callers — no signature changes needed at the 20+
serializeStatus() call sites.
2026-03-20 14:00:44 +01:00
Ricardo
5fc4d3a6f5 fix: add sparse:true to accessToken index (duplicate key on OAuth authorize)
The accessToken_1 unique index on ap_oauth_tokens lacked sparse:true.
During OAuth2 authorization, POST /oauth/authorize inserts a document
with accessToken:null (auth code phase — token not yet issued). MongoDB
unique indexes include null values by default, so only one such document
could exist. Every subsequent authorization attempt failed with E11000
duplicate key error.

Adding sparse:true skips null values in the index, allowing multiple
auth code documents to coexist while still enforcing uniqueness among
actual access tokens. This matches the code index pattern (line 1423)
which already uses sparse:true.

Note: existing deployments must drop the stale index before restart:
  mongosh $MONGODB_URL --eval 'db.ap_oauth_tokens.dropIndex("accessToken_1")'
  mongosh $MONGODB_URL --eval 'db.ap_oauth_tokens.deleteMany({accessToken:null})'

Confab-Link: http://localhost:8080/sessions/0b241cd6-aff2-4fec-853c-2b5a61e61946
2026-03-20 13:29:25 +01:00
Ricardo
2c0cfffd54 feat: add Mastodon Client API layer for Phanpy/Elk compatibility
Implement the Mastodon Client REST API (/api/v1/*, /api/v2/*) and OAuth2
server within the ActivityPub plugin, enabling Mastodon-compatible clients
to connect to the Fedify-based server.

Core features:
- OAuth2 with PKCE (S256) — app registration, authorization, token exchange
- Instance info + nodeinfo for client discovery
- Account lookup, verification, relationships, follow/unfollow/mute/block
- Home/public/hashtag timelines with cursor-based pagination
- Status viewing, creation, deletion, thread context
- Favourite, boost, bookmark interactions with AP federation
- Notifications with type filtering and pagination
- Search across accounts, statuses, and hashtags
- Markers for read position tracking
- Bookmarks and favourites collection lists
- 25+ stub endpoints preventing client errors on unimplemented features

Architecture:
- 24 new files under lib/mastodon/ (entities, helpers, middleware, routes)
- Virtual endpoint at "/" via Indiekit.addEndpoint() for domain-root access
- CORS + JSON error handling for browser-based clients
- Six-layer mute/block filtering reusing existing moderation infrastructure

BREAKING CHANGE: bumps to v3.0.0 — adds new MongoDB collections
(ap_oauth_apps, ap_oauth_tokens, ap_markers) and new route registrations

Confab-Link: http://localhost:8080/sessions/5360e3f5-b3cc-4bf3-8c31-5448e2b23947
2026-03-18 12:50:52 +01:00
Ricardo
2ca491f28b fix: wire content-warning property for CW text
Updated jf2-to-as2 and compose controller to use the renamed
"content-warning" property instead of overloading "summary" for
CW text. This pairs with the endpoint-posts fix that renamed the
CW form input to prevent collision with the summary field.

Confab-Link: http://localhost:8080/sessions/1dcdf030-8015-4d23-89da-b43fd69c7138
2026-03-18 00:24:35 +01:00
Ricardo
b4daa806bd chore: bump version to 2.15.3
Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 17:13:57 +01:00
Ricardo
26c81a6a76 fix: exclude soft-deleted posts from outbox and content negotiation
Deleted posts (with properties.deleted timestamp) were still served
via the outbox dispatcher and content negotiation catch-all. Now:
- Outbox find() and countDocuments() filter out deleted posts
- Object dispatcher returns null for deleted posts (Fedify 404)
- Content negotiation falls through to Express for deleted posts

Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 17:12:30 +01:00
Ricardo
d676374ec1 docs: document Fedify workarounds for endpoints type and PropertyValue attachment
- Gotcha 10: endpoints as:Endpoints type stripping (fedify#576, fixed in 2.1.0)
- Gotcha 11: PropertyValue attachment type validation (fedify#629, open)
- README: added both to Fedify Workarounds section
- Renumbered gotchas 10-31 → 12-33

Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 15:16:01 +01:00
Ricardo
e066222422 chore: bump version to 2.15.2
Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 15:09:31 +01:00
Ricardo
4f0d7925b2 fix: strip invalid as:Endpoints type from Fedify actor serialization
Fedify serializes the endpoints object with "type": "as:Endpoints" which
is not a valid ActivityStreams 2.0 type. This causes browser.pub validation
failures. Strip the type field in the JSON patching block.

Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 15:07:46 +01:00
Ricardo
a23c955b94 chore: bump version to 2.15.1
Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 13:17:59 +01:00
Ricardo
c8aa0383b9 feat: wire reply intelligence to frontend — timeline filtering, thread reconstruction, visibility badges
- Filter isContext items and private/direct posts from main timeline, new post count, and unread count
- Post detail: query local replies from ap_timeline before remote fetch, deduplicate, sort chronologically
- Add visibility badge (unlisted/private/direct) on item cards next to timestamp

Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 13:13:51 +01:00
Ricardo
a87fe59259 docs: update CLAUDE.md and README.md with v2.14.0/v2.15.0 features
Add full feature documentation for federation resilience (v2.14.0) and
Hollo-inspired patterns (v2.15.0). Add credits to Hollo, Fedify, and Wafrn.
Update architecture tree, collections table, routes, and gotchas in CLAUDE.md.

Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 11:23:12 +01:00
Ricardo
0d6668e741 chore: bump version to 2.15.0
Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 11:13:11 +01:00
Ricardo
206ae4c6e5 feat: Hollo-inspired federation patterns — outbox failure handling, reply chains, forwarding, visibility
- Add outbox permanent failure handling with smart cleanup:
  - 410 Gone: immediate full cleanup (follower + timeline + notifications)
  - 404: strike system (3 failures over 7+ days triggers cleanup)
  - Strike reset on inbound activity (proves actor is alive)
- Add recursive reply chain fetching (depth 5) with isContext flag
- Add reply forwarding to followers for public replies to our posts
- Add write-time visibility classification (public/unlisted/private/direct)

Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 11:11:18 +01:00
Ricardo
297b5b9464 chore: bump version to 2.14.0
Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 09:18:38 +01:00
Ricardo
1567b7c4e5 feat: operational resilience hardening — server blocking, caching, key refresh, async inbox (v2.14.0)
- Server-level blocking: O(1) Redis SISMEMBER check in all inbox listeners,
  admin UI for blocking/unblocking servers by hostname, MongoDB fallback
- Redis caching for collection dispatchers: 300s TTL on followers/following/liked
  counters and paginated pages, one-shot followers recipients cache
- Proactive key refresh: daily cron re-fetches actor documents for followers
  with 7+ day stale keys using lookupWithSecurity()
- Async inbox processing: MongoDB-backed queue with 3s polling, retry (3 attempts),
  24h TTL auto-prune. Follow keeps synchronous Accept, Block keeps synchronous
  follower removal. All other activity types fully deferred to background processor.

Inspired by wafrn's battle-tested multi-user AP implementation.

Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709
2026-03-17 09:16:05 +01:00