Commit Graph

25 Commits

Author SHA1 Message Date
Ricardo
4298a6a307 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:31:29 +01:00
svemagie
ce30dfea3b feat(activitypub): AP protocol compliance — Like id, Like dispatcher, repost commentary, ap-url API
Five improvements to strict ActivityPub protocol compliance and
real-world Mastodon interoperability:

1. allowPrivateAddress: true in createFederation (federation-setup.js)
   Fixes Fedify's SSRF guard rejecting own-site URLs that resolve to
   private IPs on the local LAN (e.g. home-network deployments where
   the blog hostname maps to 10.x.x.x internally).

2. Canonical id on Like activities (jf2-to-as2.js)
   Per AP §6.2.1, activities SHOULD have an id URI so remote servers
   can dereference them. Derives mount path from actor URL and constructs
   {publicationUrl}{mount}/activities/like/{post-path}.

3. Like activity object dispatcher (federation-setup.js)
   Per AP §3.1, objects with an id MUST be dereferenceable at that URI.
   Registers federation.setObjectDispatcher(Like, .../activities/like/{+id})
   so fetching the canonical Like URL returns the activity as AP JSON.
   Adds Like to @fedify/fedify/vocab imports.

4. Repost commentary in AP output (jf2-to-as2.js)
   - jf2ToAS2Activity: only sends Announce for pure reposts (no content);
     reposts with commentary fall through to Create(Note) with content
     formatted as "{commentary}<br><br>🔁 <url>" so followers see the text.
   - jf2ToActivityStreams: prepends commentary to the repost Note content
     for correct display in content-negotiation / search responses.

5. GET /api/ap-url public endpoint (index.js)
   Resolves a blog post URL → its Fedify-served AP object URL for use by
   "Also on Fediverse" widgets. Prevents nginx from intercepting
   authorize_interaction requests that need AP JSON.
   Special case: AP-likes return { apUrl: likeOf } so authorize_interaction
   opens the original remote post rather than the blog's like post.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:12:21 +01:00
svemagie
842fc5af2a feat(like): send Like activity for AP objects, bookmark for regular URLs
When the `like-of` URL serves ActivityPub content (detected via content
negotiation with Accept: application/activity+json), deliver a proper
`Like { actor, object, to: Public }` activity to followers.

For likes of regular (non-AP) URLs, fall through to the existing
bookmark-style `Create(Note)` behaviour (🔖 content with #bookmark tag).

- Add `isApUrl()` async helper (3 s timeout, fails silently)
- Make `jf2ToAS2Activity` async; add Like detection before repost block
- Update all four call sites in federation-setup.js and index.js

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 08:50:00 +01:00
svemagie
b99f5fb73e Merge upstream rmdes:main — v2.13.0–v2.15.4 into svemagie/main
New upstream features:
- v2.13.0: FEP-8fcf/fe34 compliance, custom emoji, manual follow approval
- v2.14.0: Server blocking, Redis caching, key refresh, async inbox queue
- v2.15.0: Outbox failure handling (strike system), reply chain forwarding
- v2.15.1: Reply intelligence in reader (visibility badges, thread reconstruction)
- v2.15.2: Strip invalid as:Endpoints type from actor serialization
- v2.15.3: Exclude soft-deleted posts from outbox/content negotiation
- v2.15.4: Wire content-warning property for CW text

Conflict resolution:
- federation-setup.js: merged our draft/unlisted/visibility filters with
  upstream's soft-delete filter
- compose.js: kept our DM compose path, adopted upstream's
  lookupWithSecurity for remote object resolution
- notifications.js: kept our separate reply/mention tabs, added upstream's
  follow_request grouping
- inbox-listeners.js: took upstream's thin-shim rewrite (handlers moved to
  inbox-handlers.js which already has DM detection)
- notification-card.njk: merged DM badge with follow_request support

Preserved from our fork:
- Like/Announce to:Public cc:followers addressing
- Nested tag normalization (cat.split("/").at(-1))
- DM compose/reply path in compose controller

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 00:42:31 +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
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
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
svemagie
8b9bff4d2e fix: AP inbox reliability — PeerTube View skip, raw body digest, signature window, trailing slash
federation-bridge.js:
- Buffer application/activity+json and ld+json bodies that Express
  doesn't parse (inbox POSTs from Mastodon, PeerTube, etc.)
- Store original bytes in req._rawBody and pass them verbatim to Fedify
  so HTTP Signature Digest verification passes; JSON.stringify reorders
  keys which caused every Mastodon Like/Announce/Create to be silently
  rejected
- Short-circuit PeerTube View (WatchAction) activities with 200 before
  Fedify's JSON-LD parser throws on Schema.org extensions

federation-setup.js:
- Accept signatures up to 12 hours old (Mastodon retries with the
  original signature hours after a failed delivery)
- Look up AP object URLs with $in [url, url+"/"] to tolerate trailing
  slash differences between stored posts and AP object URLs

inbox-listeners.js:
- Register a no-op .on(View) handler so Fedify doesn't log noisy
  "Unsupported activity type" errors for PeerTube watch events

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 12:13:47 +01:00
svemagie
39d45ec04e feat: integrate docloader loglevel, unlisted guards, alias-clear, likePost/boostPost
federation-setup.js:
- Suppress fedify docloader logs below fatal level to reduce noise from
  deleted remote actors (404/410)
- Add visibility:unlisted guard to outbox dispatcher, counter, and
  resolvePost object dispatcher

controllers/migrate.js:
- Allow clearing alsoKnownAs by detecting submitted empty aliasUrl field
  via hasOwnProperty check (previously only set when non-empty)

index.js:
- Add resolveAuthor import
- Skip federation for unlisted posts in syndicate()
- Add likePost(postUrl, collections) — sends AP Like activity to author
- Add boostPost(postUrl, collections) — sends AP Announce to followers
  and directly to the post author's inbox

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 08:08:23 +01:00
Ricardo
abf1b94bd6 feat: migrate Fedify KV store and plugin cache from MongoDB to Redis
Replace unbounded ap_kv MongoDB collection (169K docs, 49MB) with Redis:
- Fedify KV store uses @fedify/redis RedisKvStore (native TTL support)
- Plugin cache (fedidb, batch-refollow state, migration flags) uses new
  redis-cache.js utility with indiekit: key prefix
- All controllers updated to remove kvCollection parameter passing
- Addresses OOM kills caused by ap_kv growing ~14K entries/day
2026-03-01 16:26:17 +01:00
Ricardo
17b0f582d1 feat: add remote follow / authorize interaction support
Add OStatus subscribe template to WebFinger responses so remote servers
(WordPress AP, Misskey, etc.) can discover and redirect users to complete
follow interactions. Unauthenticated users are sent to login first, then
redirected to the existing reader profile page with follow/unfollow UI.
2026-02-24 15:00:40 +01:00
Ricardo
a62a0d8aac fix: Fediverse field display text for Mastodon verification
Mastodon 4.5's extract_url_from_html requires element['href'] ==
element.text for a field to be verifiable. Using @handle@domain as
display text caused value_for_verification to return nil, making
the field permanently unverifiable.

Change to using the full actor URL as display text so the href and
text content match.
2026-02-24 14:05:03 +01:00
Ricardo
963dcc3014 feat: add Fediverse field to actor profile and fix Update compaction
Always include a "Fediverse" PropertyValue attachment in the actor
with the canonical @handle@domain address. This ensures 2+ attachments
when combined with user-defined fields, preventing Fedify's JSON-LD
compaction from collapsing single-element arrays to plain objects
(which Mastodon's update_account_fields silently rejects).

Also fixes the root cause of profile fields not appearing on Mastodon
for existing followers: Update(Person) activities were being sent with
compacted attachment objects that Mastodon ignored.
2026-02-24 12:07:50 +01:00
Ricardo
dd9bba711f feat: migrate to Fedify 2.0 with debug dashboard and modular imports
- 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)
2026-02-22 14:28:31 +01:00
Ricardo
c648606525 fix: broadcastActorUpdate was silently failing due to Fedify API mismatch
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.
2026-02-22 11:27:35 +01:00
Ricardo
0fa446ceb2 feat: make Fedify log level configurable via logLevel option
Default changed from "info" to "warning" so production logs are quiet.
Set logLevel to "info" or "debug" in config to troubleshoot federation.
2026-02-21 22:51:07 +01:00
Ricardo
55e9311c4a feat: broadcast Update(Person) on profile/featured/tags changes, fix rel=me
- 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
2026-02-21 12:19:22 +01:00
Ricardo
81a28ef086 feat: add actor type selector and profile links to admin UI
- 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
2026-02-21 10:07:03 +01:00
Ricardo
4827a98614 feat: Fedify feature completeness — collections, dispatchers, delivery hardening
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.
2026-02-20 22:57:41 +01:00
Ricardo
be369b1677 feat: federation hardening — persistent keys, Redis queue, indexes
- 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
2026-02-20 16:33:13 +01:00
Ricardo
656b66c780 fix: add WebFinger handle mapper and Ed25519 key pair
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.
2026-02-19 20:18:16 +01:00
Ricardo
53db99400a fix: suppress LogTape context-local storage warning
Add AsyncLocalStorage to LogTape configure() to fix the repeated
"Context-local storage is not configured" warning that appeared
before every Fedify log entry. Also remove unused getLogger import.
2026-02-19 19:52:05 +01:00
Ricardo
420973e5ec fix: start delivery queue and enable authenticated shared inbox fetches
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.
2026-02-19 19:34:53 +01:00
Ricardo
fb8d90232b feat: add Fedify LogTape logging for delivery visibility
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.
2026-02-19 19:19:15 +01:00
Ricardo
eaf0f1d126 feat: migrate to Fedify for ActivityPub federation (v0.2.0)
Replace hand-rolled federation code with Fedify's battle-tested
implementation. This gives us proper HTTP Signatures, WebFinger,
NodeInfo, typed inbox listeners, and collection dispatchers out
of the box.

New modules:
- lib/federation-setup.js — Fedify Federation configuration
- lib/federation-bridge.js — Express↔Fedify middleware bridge
- lib/inbox-listeners.js — typed inbox handlers (Follow, Undo, etc.)
- lib/kv-store.js — MongoDB-backed KvStore adapter
- lib/controllers/profile.js — admin profile management
- views/activitypub-profile.njk — profile editing form

Removed hand-rolled modules:
- lib/actor.js, lib/federation.js, lib/inbox.js
- lib/keys.js, lib/webfinger.js

Key changes:
- Actor, inbox, outbox, followers, following all delegate to Fedify
- Syndication uses ctx.sendActivity() instead of manual delivery
- Profile managed via admin UI, stored in ap_profile collection
- Legacy PKCS#8 keys imported via Web Crypto API
- Custom bridge preserves Express mount path (req.originalUrl)
2026-02-19 11:59:23 +01:00