From b81ecbcaa41cb3944d71171e1005f8190ffbdfb0 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Sat, 21 Feb 2026 17:06:11 +0100 Subject: [PATCH] docs: add CLAUDE.md for AI agents and README.md for humans CLAUDE.md covers architecture, 18 critical gotchas distilled from bug fixes (Fedify bridge, objectId vs getObject, template collisions, Express 5 redirect, date handling, author fallback chain, etc.), MongoDB collections, route table, and publishing workflow. README.md covers features, installation, configuration, nginx setup, how syndication/inbox/content negotiation work, Mastodon migration, admin UI reference, and known limitations. --- CLAUDE.md | 303 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 267 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 570 insertions(+) create mode 100644 CLAUDE.md create mode 100644 README.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f171052 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,303 @@ +# CLAUDE.md — @rmdes/indiekit-endpoint-activitypub + +AI agent instructions for working on this codebase. Read this entire file before making any changes. + +## What This Is + +An Indiekit plugin that adds full ActivityPub federation via [Fedify](https://fedify.dev). It turns an Indiekit-powered IndieWeb site into a fediverse actor — discoverable, followable, and interactive from Mastodon, Misskey, Pixelfed, Lemmy, etc. + +**npm:** `@rmdes/indiekit-endpoint-activitypub` +**Version:** See `package.json` +**Node:** >=22 +**Module system:** ESM (`"type": "module"`) + +## Architecture Overview + +``` +index.js ← Plugin entry, route registration, syndicator +├── lib/federation-setup.js ← Fedify Federation instance, dispatchers, collections +├── lib/federation-bridge.js ← Express ↔ Fedify request/response bridge +├── lib/inbox-listeners.js ← Handlers for Follow, Undo, Like, Announce, Create, Delete, etc. +├── lib/jf2-to-as2.js ← JF2 → ActivityStreams conversion (plain JSON + Fedify vocab) +├── lib/kv-store.js ← MongoDB-backed KvStore for Fedify +├── lib/activity-log.js ← Activity logging to ap_activities +├── lib/timeline-store.js ← Timeline item extraction + sanitization +├── lib/timeline-cleanup.js ← Retention-based timeline pruning +├── lib/batch-refollow.js ← Gradual re-follow for imported Mastodon accounts +├── lib/migration.js ← CSV parsing + WebFinger resolution for Mastodon import +├── lib/csrf.js ← CSRF token generation/validation +├── lib/storage/ +│ ├── timeline.js ← Timeline CRUD with cursor pagination +│ ├── notifications.js ← Notification CRUD with read/unread tracking +│ └── moderation.js ← Mute/block storage +├── lib/controllers/ ← Express route handlers (admin UI) +│ ├── dashboard.js, reader.js, compose.js, profile.js, profile.remote.js +│ ├── followers.js, following.js, activities.js +│ ├── featured.js, featured-tags.js +│ ├── interactions.js, interactions-like.js, interactions-boost.js +│ ├── moderation.js, migrate.js, refollow.js +├── views/ ← Nunjucks templates +│ ├── activitypub-*.njk ← Page templates +│ ├── layouts/ap-reader.njk ← Reader layout (NOT reader.njk — see gotcha below) +│ └── partials/ ← Shared components +├── assets/ +│ ├── reader.css ← Reader UI styles +│ └── icon.svg ← Plugin icon +└── locales/en.json ← i18n strings +``` + +## Data Flow + +``` +Outbound: Indiekit post → syndicator.syndicate() → jf2ToAS2Activity() → ctx.sendActivity() → follower inboxes +Inbound: Remote inbox POST → Fedify → inbox-listeners.js → MongoDB collections → admin UI +Reader: Followed account posts → Create inbox → timeline-store → ap_timeline → reader UI +``` + +## MongoDB Collections + +| Collection | Purpose | Key fields | +|---|---|---| +| `ap_followers` | Accounts following us | `actorUrl` (unique), `inbox`, `sharedInbox`, `source` | +| `ap_following` | Accounts we follow | `actorUrl` (unique), `source`, `acceptedAt` | +| `ap_activities` | Activity log (TTL-indexed) | `direction`, `type`, `actorUrl`, `objectUrl`, `receivedAt` | +| `ap_keys` | Cryptographic key pairs | `type` ("rsa" or "ed25519"), key material | +| `ap_kv` | Fedify KvStore + job state | `_id` (key path), `value` | +| `ap_profile` | Actor profile (single doc) | `name`, `summary`, `icon`, `attachments`, `actorType` | +| `ap_featured` | Pinned posts | `postUrl`, `pinnedAt` | +| `ap_featured_tags` | Featured hashtags | `tag`, `addedAt` | +| `ap_timeline` | Reader timeline items | `uid` (unique), `published`, `author`, `content` | +| `ap_notifications` | Likes, boosts, follows, mentions | `uid` (unique), `type`, `read` | +| `ap_muted` | Muted actors/keywords | `url` or `keyword` | +| `ap_blocked` | Blocked actors | `url` | +| `ap_interactions` | Like/boost tracking per post | `objectUrl`, `type` | + +## Critical Patterns and Gotchas + +### 1. Express ↔ Fedify Bridge (CUSTOM — NOT @fedify/express) + +We **cannot** use `@fedify/express`'s `integrateFederation()` because Indiekit mounts plugins at sub-paths. Express strips the mount prefix from `req.url`, breaking Fedify's URI template matching. Instead, `federation-bridge.js` uses `req.originalUrl` to build the full URL. + +**If you see path-matching issues with Fedify, check that `req.originalUrl` is being used, not `req.url`.** + +### 2. Content Negotiation Route — GET Only + +The `contentNegotiationRoutes` router is mounted at `/` (root). It MUST only pass `GET`/`HEAD` requests to Fedify. Passing `POST`/`PUT`/`DELETE` would cause `fromExpressRequest()` to consume the body stream via `Readable.toWeb(req)`, breaking Express body-parsed routes downstream (admin forms, Micropub, etc.). + +### 3. Skip Fedify for Admin Routes + +In `routesPublic`, the middleware skips paths starting with `/admin`. Without this, Fedify would intercept admin UI requests and return 404/406 responses instead of letting Express serve the authenticated pages. + +### 4. Use .objectId/.actorId — NOT .getObject()/.getActor() in Inbox Handlers + +Fedify's `.getObject()` and `.getActor()` trigger HTTP fetches to remote servers. This fails silently or retries ~10 times when: +- Remote server has **Authorized Fetch** enabled (returns 401) +- Server is down or unreachable +- Object has been deleted + +**Always prefer** `.objectId?.href` and `.actorId?.href` (zero network requests) for Like, Announce, Undo, and Delete handlers. Only use `.getObject()` / `.getActor()` when you need the full object, and **always wrap in try-catch**. + +### 5. Accept(Follow) Matching — Don't Check Inner Object Type + +Fedify often resolves the inner object of `Accept` to a `Person` (the Follow's target) rather than the `Follow` itself. The Accept handler matches against `ap_following` by actor URL instead of inspecting `inner instanceof Follow`. + +### 6. Filter Inbound Likes/Announces to Our Content Only + +Without filtering, the inbox logs every Like/Announce from every federated server — including reactions to other people's content that happens to flow through shared inboxes. Check `objectId.startsWith(publicationUrl)` before logging. + +### 7. Nunjucks Template Name Collisions + +Template names resolve across ALL registered plugin view directories. If two plugins have `views/layouts/reader.njk`, Nunjucks loads whichever it finds first (often wrong). The reader layout is named `ap-reader.njk` to avoid collision with `@rmdes/indiekit-endpoint-microsub`'s `reader.njk`. + +**Never name a layout/template with a generic name that another plugin might use.** + +### 8. Express 5 — No redirect("back") + +Express 5 removed the `"back"` magic keyword from `response.redirect()`. It's treated as a literal URL, causing 404s at paths like `/admin/featured/back`. Always use explicit redirect paths. + +### 9. Fedify Endpoints Type Bug (Workaround) + +Fedify serializes `endpoints` with `"type": "as:Endpoints"` which is not a real ActivityStreams type. `sendFedifyResponse()` in `federation-bridge.js` strips this from actor JSON responses. Remove the workaround when [fedify#576](https://github.com/fedify-dev/fedify/issues/576) is fixed upstream. + +### 10. Profile Links — Express qs Body Parser Key Mismatch + +`express.urlencoded({ extended: true })` uses `qs` which strips `[]` from array field names. HTML fields named `link_name[]` arrive as `request.body.link_name` (not `request.body["link_name[]"]`). The profile controller reads `link_name` and `link_value`, NOT `link_name[]`. + +### 11. Author Resolution Fallback Chain + +`extractObjectData()` in `timeline-store.js` uses a multi-strategy fallback: +1. `object.getAttributedTo()` — async, may fail with Authorized Fetch +2. `options.actorFallback` — the activity's actor (passed from Create handler) +3. `object.attribution` / `object.attributedTo` — plain object properties +4. `object.attributionIds` — non-fetching URL array with username extraction from common patterns (`/@name`, `/users/name`) + +Without this chain, many timeline items show "Unknown" as the author. + +### 12. Username Extraction from Actor URLs + +When extracting usernames from attribution IDs, handle multiple URL patterns: +- `/@username` (Mastodon) +- `/users/username` (Mastodon, Indiekit) +- `/ap/users/12345/` (numeric IDs on some platforms) + +The regex was previously matching "users" instead of the actual username from `/users/NatalieDavis`. + +### 13. Empty Boost Filtering + +Lemmy/PieFed send Announce activities where the boosted object resolves to an activity ID instead of a Note/Article with actual content. Check `object.content || object.name` before storing to avoid empty cards in the timeline. + +### 14. Temporal.Instant for Fedify Dates + +Fedify uses `@js-temporal/polyfill` for dates. When setting `published` on Fedify objects, use `Temporal.Instant.from(isoString)`. When reading Fedify dates in inbox handlers, use `String(object.published)` to get ISO strings — NOT `new Date(object.published)` which causes `TypeError`. + +### 15. LogTape — Configure Once Only + +`@logtape/logtape`'s `configure()` can only be called once per process. The module-level `_logtapeConfigured` flag prevents duplicate configuration. If configure fails (e.g., another plugin already configured it), catch the error silently. + +### 16. .authorize() Intentionally NOT Chained on Actor Dispatcher + +Fedify's `.authorize()` triggers HTTP Signature verification on every GET to the actor endpoint. Servers requiring Authorized Fetch cause infinite loops: Fedify tries to fetch their key → they return 401 → Fedify retries → 500 errors. Re-enable when Fedify supports authenticated document loading for outgoing fetches. + +### 17. Delivery Queue Must Be Started + +`federation.startQueue()` MUST be called after setup. Without it, `ctx.sendActivity()` enqueues tasks but the message queue never processes them — activities are never delivered. + +### 18. Shared Key Dispatcher for Shared Inbox + +`inboxChain.setSharedKeyDispatcher()` tells Fedify to use our actor's key pair when verifying HTTP Signatures on the shared inbox. Without this, servers like hachyderm.io (which requires Authorized Fetch) have their signatures rejected. + +## Date Handling Convention + +**All dates MUST be stored as ISO 8601 strings.** This is mandatory across all Indiekit plugins. + +```javascript +// CORRECT +followedAt: new Date().toISOString() +published: String(fedifyObject.published) // Temporal → string + +// WRONG — crashes Nunjucks | date filter +followedAt: new Date() +published: new Date(fedifyObject.published) +``` + +The Nunjucks `| date` filter calls `date-fns parseISO()` which only accepts ISO strings. `Date` objects cause `"dateString.split is not a function"` crashes. + +## Batch Re-follow State Machine + +``` +import → refollow:pending → refollow:sent → federation (happy path: Accept received) +import → refollow:pending → refollow:sent → refollow:failed (after 3 retries) +``` + +- `import`: Imported from Mastodon CSV, no Follow sent yet +- `refollow:pending`: Claimed by batch processor, being processed +- `refollow:sent`: Follow activity sent, awaiting Accept +- `federation`: Accept received, fully federated +- `refollow:failed`: Max retries exceeded + +On restart, `refollow:pending` entries are reset to `import` to prevent stale claims. + +## Plugin Lifecycle + +1. `constructor()` — Merges options with defaults +2. `init(Indiekit)` — Called by Indiekit during startup: + - Stores `publication.me` as `_publicationUrl` + - Registers 13 MongoDB collections with indexes + - Seeds actor profile from config (first run only) + - Calls `setupFederation()` which creates Fedify instance + starts queue + - Registers endpoint (mounts routes) and syndicator + - Starts batch re-follow processor (10s delay) + - Schedules timeline cleanup (on startup + every 24h) + +## Route Structure + +| Method | Path | Handler | Auth | +|---|---|---|---| +| `*` | `/.well-known/*` | Fedify (WebFinger, NodeInfo) | No | +| `*` | `{mount}/users/*`, `{mount}/inbox` | Fedify (actor, inbox, outbox, collections) | No (HTTP Sig) | +| `GET` | `{mount}/` | Dashboard | Yes (IndieAuth) | +| `GET` | `{mount}/admin/reader` | Timeline reader | Yes | +| `GET` | `{mount}/admin/reader/notifications` | Notifications | Yes | +| `POST` | `{mount}/admin/reader/compose` | Compose reply | Yes | +| `POST` | `{mount}/admin/reader/like,unlike,boost,unboost` | Interactions | Yes | +| `POST` | `{mount}/admin/reader/follow,unfollow` | Follow/unfollow | Yes | +| `GET` | `{mount}/admin/reader/profile` | Remote profile view | Yes | +| `GET` | `{mount}/admin/reader/moderation` | Moderation dashboard | Yes | +| `POST` | `{mount}/admin/reader/mute,unmute,block,unblock` | Moderation actions | Yes | +| `GET` | `{mount}/admin/followers,following,activities` | Lists | Yes | +| `GET/POST` | `{mount}/admin/profile` | Actor profile editor | Yes | +| `GET/POST` | `{mount}/admin/featured` | Pinned posts | Yes | +| `GET/POST` | `{mount}/admin/tags` | Featured tags | Yes | +| `GET/POST` | `{mount}/admin/migrate` | Mastodon migration | Yes | +| `*` | `{mount}/admin/refollow/*` | Batch refollow control | Yes | +| `GET` | `/*` (root) | Content negotiation (AP clients only) | No | + +## Dependencies + +| Package | Purpose | +|---|---| +| `@fedify/fedify` | ActivityPub federation framework | +| `@fedify/express` | Express integration utilities (types only — bridge is custom) | +| `@fedify/redis` | Redis message queue for delivery | +| `@js-temporal/polyfill` | Temporal API for Fedify date handling | +| `ioredis` | Redis client | +| `sanitize-html` | XSS prevention for timeline/notification content | +| `express` | Route handling (peer: Indiekit provides it) | + +## Configuration Options + +```javascript +{ + mountPath: "/activitypub", // URL prefix for all routes + actor: { + handle: "rick", // Fediverse username + name: "Ricardo Mendes", // Display name (seeds profile) + summary: "", // Bio (seeds profile) + icon: "", // Avatar URL (seeds profile) + }, + checked: true, // Syndicator checked by default + alsoKnownAs: "", // Mastodon migration alias + activityRetentionDays: 90, // TTL for ap_activities (0 = forever) + storeRawActivities: false, // Store full JSON of inbound activities + redisUrl: "", // Redis for delivery queue (empty = in-process) + parallelWorkers: 5, // Parallel delivery workers (with Redis) + actorType: "Person", // Person | Service | Organization | Group + timelineRetention: 1000, // Max timeline items (0 = unlimited) +} +``` + +## Publishing Workflow + +1. Edit code in this repo +2. Bump version in `package.json` (npm rejects duplicate versions) +3. Commit and push +4. **STOP** — user must run `npm publish` manually (requires OTP) +5. After publish confirmation, update Dockerfile version in `indiekit-cloudron/` +6. `cloudron build --no-cache && cloudron update --app rmendes.net --no-backup` + +## Testing + +No automated test suite. Manual testing against real fediverse servers: + +```bash +# WebFinger +curl -s "https://rmendes.net/.well-known/webfinger?resource=acct:rick@rmendes.net" | jq . + +# Actor document +curl -s -H "Accept: application/activity+json" "https://rmendes.net/" | jq . + +# NodeInfo +curl -s "https://rmendes.net/nodeinfo/2.1" | jq . + +# Search from Mastodon for @rick@rmendes.net +``` + +## CSS Conventions + +The reader CSS (`assets/reader.css`) uses Indiekit's theme custom properties for automatic dark mode support: +- `--color-on-background` (not `--color-text`) +- `--color-on-offset` (not `--color-text-muted`) +- `--border-radius-small` (not `--border-radius`) +- `--color-red45`, `--color-green50`, etc. (not hardcoded hex) + +Post types are differentiated by left border color: purple (notes), green (articles), yellow (boosts), primary (replies). diff --git a/README.md b/README.md new file mode 100644 index 0000000..23b2325 --- /dev/null +++ b/README.md @@ -0,0 +1,267 @@ +# @rmdes/indiekit-endpoint-activitypub + +ActivityPub federation endpoint for [Indiekit](https://getindiekit.com). Makes your IndieWeb site a full fediverse actor — discoverable, followable, and interactive from Mastodon, Misskey, Pixelfed, and any ActivityPub-compatible platform. + +## Features + +**Federation** +- Full ActivityPub actor with WebFinger, NodeInfo, HTTP Signatures, and Object Integrity Proofs (Ed25519) +- Outbox syndication — posts created via Micropub are automatically delivered to followers +- Inbox processing — receives follows, likes, boosts, replies, mentions, deletes, and account moves +- Content negotiation — ActivityPub clients requesting your site get JSON-LD; browsers get HTML +- Reply delivery — replies are addressed to and delivered directly to the original post's author +- Shared inbox support with collection sync (FEP-8fcf) +- Configurable actor type (Person, Service, Organization, Group) + +**Reader** +- Timeline view showing posts from followed accounts +- Notifications for likes, boosts, follows, mentions, and replies +- Compose form with dual-path posting (quick AP reply or Micropub blog post) +- Native interactions (like, boost, reply, follow/unfollow from the reader) +- Remote actor profile pages +- Content warnings and sensitive content handling +- Media display (images, video, audio) +- Configurable timeline retention + +**Moderation** +- Mute actors or keywords +- Block actors (also removes from followers) +- All moderation actions available from the reader UI + +**Mastodon Migration** +- Import following/followers lists from Mastodon CSV exports +- Set `alsoKnownAs` alias for account Move verification +- Batch re-follow processor — gradually sends Follow activities to imported accounts +- Progress tracking with pause/resume controls + +**Admin UI** +- Dashboard with follower/following counts and recent activity +- Profile editor (name, bio, avatar, header, profile links with rel="me" verification) +- Pinned posts (featured collection) +- Featured tags (hashtag collection) +- Activity log (inbound/outbound) +- Follower and following lists with source tracking + +## Requirements + +- [Indiekit](https://getindiekit.com) v1.0.0-beta.25+ +- Node.js >= 22 +- MongoDB (used by Indiekit) +- Redis (recommended for production delivery queue; in-process queue available for development) + +## Installation + +```bash +npm install @rmdes/indiekit-endpoint-activitypub +``` + +## Configuration + +Add the plugin to your Indiekit config: + +```javascript +// indiekit.config.js +export default { + plugins: [ + "@rmdes/indiekit-endpoint-activitypub", + ], + "@rmdes/indiekit-endpoint-activitypub": { + mountPath: "/activitypub", + actor: { + handle: "yourname", + name: "Your Name", + summary: "A short bio", + icon: "https://example.com/avatar.jpg", + }, + }, +}; +``` + +### All Options + +| Option | Type | Default | Description | +|---|---|---|---| +| `mountPath` | string | `"/activitypub"` | URL prefix for all plugin routes | +| `actor.handle` | string | `"rick"` | Fediverse username (e.g. `@handle@yourdomain.com`) | +| `actor.name` | string | `""` | Display name (used to seed profile on first run) | +| `actor.summary` | string | `""` | Bio text (used to seed profile on first run) | +| `actor.icon` | string | `""` | Avatar URL (used to seed profile on first run) | +| `checked` | boolean | `true` | Whether the syndicator is checked by default in the post editor | +| `alsoKnownAs` | string | `""` | Mastodon migration alias URL | +| `activityRetentionDays` | number | `90` | Days to keep activity log entries (0 = forever) | +| `storeRawActivities` | boolean | `false` | Store full raw JSON of inbound activities | +| `redisUrl` | string | `""` | Redis connection URL for delivery queue | +| `parallelWorkers` | number | `5` | Number of parallel delivery workers (requires Redis) | +| `actorType` | string | `"Person"` | Actor type: `Person`, `Service`, `Organization`, or `Group` | +| `timelineRetention` | number | `1000` | Maximum timeline items to keep (0 = unlimited) | + +### Redis (Recommended for Production) + +Without Redis, the plugin uses an in-process message queue. This works for development but won't survive restarts and has limited throughput. + +```javascript +"@rmdes/indiekit-endpoint-activitypub": { + redisUrl: "redis://localhost:6379", + parallelWorkers: 5, +}, +``` + +### Nginx Configuration (Reverse Proxy) + +If you serve a static site alongside Indiekit (e.g. with Eleventy), you need nginx rules to route ActivityPub requests to Indiekit while serving HTML to browsers: + +```nginx +# ActivityPub content negotiation — detect AP clients +map $http_accept $is_activitypub { + default 0; + "~*application/activity\+json" 1; + "~*application/ld\+json" 1; +} + +# Proxy /activitypub to Indiekit +location /activitypub { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto https; +} + +# Default: static site, but AP clients get proxied +location / { + if ($is_activitypub) { + proxy_pass http://127.0.0.1:8080; + } + try_files $uri $uri/ $uri.html =404; +} +``` + +## How It Works + +### Syndication (Outbound) + +When you create a post via Micropub, Indiekit's syndication system calls this plugin's syndicator. The plugin: + +1. Converts the JF2 post properties to an ActivityStreams 2.0 `Create(Note)` or `Create(Article)` activity +2. For replies, resolves the original post's author to include them in CC and deliver directly to their inbox +3. Sends the activity to all followers via shared inboxes using Fedify's delivery queue +4. Appends a permalink to the content so fediverse clients link back to your canonical post + +### Inbox Processing (Inbound) + +When remote servers send activities to your inbox: + +- **Follow** → Auto-accepted, stored in `ap_followers`, notification created +- **Undo(Follow)** → Removed from `ap_followers` +- **Like** → Logged in activity log, notification created (only for reactions to your own posts) +- **Announce (Boost)** → Logged + notification (your content) or stored in timeline (followed account) +- **Create (Note/Article)** → Stored in timeline if from a followed account; notification if it's a reply or mention +- **Update** → Updates timeline item content or refreshes follower profile data +- **Delete** → Removes from activity log and timeline +- **Move** → Updates follower's actor URL +- **Accept(Follow)** → Marks our follow as accepted +- **Reject(Follow)** → Marks our follow as rejected +- **Block** → Removes actor from our followers + +### Content Negotiation + +The plugin mounts a root-level router that intercepts requests from ActivityPub clients (detected by `Accept: application/activity+json` or `application/ld+json`): + +- Root URL (`/`) → Redirects to the Fedify actor document +- Post URLs → Looks up the post in MongoDB, converts to AS2 JSON +- NodeInfo (`/nodeinfo/2.1`) → Delegated to Fedify + +Regular browser requests pass through unmodified. + +### Mastodon Migration + +The plugin supports migrating from a Mastodon account: + +1. **Set alias** — Configure `alsoKnownAs` with your old Mastodon profile URL. This is verified by Mastodon before allowing a Move. +2. **Import social graph** — Upload Mastodon's `following_accounts.csv` and `followers.csv` exports. Following entries are resolved via WebFinger and stored locally. +3. **Trigger Move** — From Mastodon's settings, initiate a Move to `@handle@yourdomain.com`. Mastodon notifies your followers, and compatible servers auto-refollow. +4. **Batch re-follow** — The plugin gradually sends Follow activities to all imported accounts (10 per batch, 30s between batches) so remote servers start delivering content to your inbox. + +## Verification + +After deployment, verify federation is working: + +```bash +# WebFinger discovery +curl -s "https://yourdomain.com/.well-known/webfinger?resource=acct:handle@yourdomain.com" | jq . + +# Actor document +curl -s -H "Accept: application/activity+json" "https://yourdomain.com/" | jq . + +# NodeInfo +curl -s "https://yourdomain.com/nodeinfo/2.1" | jq . +``` + +Then search for `@handle@yourdomain.com` from any Mastodon instance — your profile should appear. + +## Admin UI Pages + +All admin pages are behind IndieAuth authentication: + +| Page | Path | Description | +|---|---|---| +| Dashboard | `/activitypub` | Overview with follower/following counts, recent activity | +| Reader | `/activitypub/admin/reader` | Timeline from followed accounts | +| Notifications | `/activitypub/admin/reader/notifications` | Likes, boosts, follows, mentions, replies | +| Compose | `/activitypub/admin/reader/compose` | Reply composer (quick AP or Micropub) | +| Moderation | `/activitypub/admin/reader/moderation` | Muted/blocked accounts and keywords | +| Profile | `/activitypub/admin/profile` | Edit actor display name, bio, avatar, links | +| Followers | `/activitypub/admin/followers` | List of accounts following you | +| Following | `/activitypub/admin/following` | List of accounts you follow | +| Activity Log | `/activitypub/admin/activities` | Inbound/outbound activity history | +| Pinned Posts | `/activitypub/admin/featured` | Pin/unpin posts to your featured collection | +| Featured Tags | `/activitypub/admin/tags` | Add/remove featured hashtags | +| Migration | `/activitypub/admin/migrate` | Mastodon import wizard | + +## MongoDB Collections + +The plugin creates these collections automatically: + +| Collection | Description | +|---|---| +| `ap_followers` | Accounts following your actor | +| `ap_following` | Accounts you follow | +| `ap_activities` | Activity log with automatic TTL cleanup | +| `ap_keys` | RSA and Ed25519 key pairs for HTTP Signatures | +| `ap_kv` | Fedify key-value store and batch job state | +| `ap_profile` | Actor profile (single document) | +| `ap_featured` | Pinned/featured posts | +| `ap_featured_tags` | Featured hashtags | +| `ap_timeline` | Reader timeline items from followed accounts | +| `ap_notifications` | Interaction notifications | +| `ap_muted` | Muted actors and keywords | +| `ap_blocked` | Blocked actors | +| `ap_interactions` | Per-post like/boost tracking | + +## Supported Post Types + +The JF2-to-ActivityStreams converter handles these Indiekit post types: + +| Post Type | ActivityStreams | +|---|---| +| note, reply, bookmark, jam, rsvp, checkin | `Create(Note)` | +| article | `Create(Article)` | +| like | `Like` | +| repost | `Announce` | +| photo, video, audio | Attachments on Note/Article | + +Categories are converted to `Hashtag` tags. Bookmarks include a bookmark emoji and link. + +## Known Limitations + +- **No automated tests** — Manual testing against real fediverse servers +- **Single actor** — One fediverse identity per Indiekit instance +- **No Authorized Fetch enforcement** — Disabled due to Fedify's current limitation with authenticated outgoing fetches (causes infinite loops with servers that require it) +- **No image upload in reader** — Compose form is text-only +- **In-process queue without Redis** — Activities may be lost on restart + +## License + +MIT + +## Author + +[Ricardo Mendes](https://rmendes.net) ([@rick@rmendes.net](https://rmendes.net))