diff --git a/CHANGES.md b/CHANGES.md
index a2600fbc9..aedb2e40f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -10,6 +10,15 @@ To be released.
### @fedify/fedify
+ - Shipped an [Agent Skills] bundle at *skills/fedify/* and declared it in
+ *package.json* through the `agents.skills` field. The skill teaches AI
+ coding agents how to *use* Fedify inside a consumer's project (builder
+ pattern, dispatchers, framework integrations, vocabulary, keys, queues
+ and storage, observability, CLI, and common pitfalls). Projects that
+ run a tool implementing the Agent Skills spec, such as [skills-npm],
+ will pick up the skill automatically from *node\_modules*, keeping the
+ guidance in sync with the installed Fedify version. [[#711], [#712]]
+
- Added `setOutboxListeners()` and `OutboxContext` for handling
client-to-server `POST` requests to actor outboxes. Outbox listeners use
application-defined authorization through `.authorize()`, catch activity
@@ -33,10 +42,14 @@ To be released.
`getAuthenticatedDocumentLoader()` now also respects
`GetAuthenticatedDocumentLoaderOptions.maxRedirection`.
+[Agent Skills]: https://agentskills.io/
+[skills-npm]: https://github.com/antfu/skills-npm
[#430]: https://github.com/fedify-dev/fedify/issues/430
[#644]: https://github.com/fedify-dev/fedify/issues/644
[#680]: https://github.com/fedify-dev/fedify/pull/680
[#688]: https://github.com/fedify-dev/fedify/pull/688
+[#711]: https://github.com/fedify-dev/fedify/issues/711
+[#712]: https://github.com/fedify-dev/fedify/pull/712
### @fedify/lint
diff --git a/packages/fedify/package.json b/packages/fedify/package.json
index 732a665ac..f9b8f9ef8 100644
--- a/packages/fedify/package.json
+++ b/packages/fedify/package.json
@@ -32,8 +32,14 @@
},
"type": "module",
"files": [
- "dist"
+ "dist",
+ "skills"
],
+ "agents": {
+ "skills": [
+ { "name": "fedify", "path": "./skills/fedify" }
+ ]
+ },
"module": "./dist/mod.js",
"main": "./dist/mod.cjs",
"types": "./dist/mod.d.ts",
diff --git a/packages/fedify/skills/fedify/SKILL.md b/packages/fedify/skills/fedify/SKILL.md
new file mode 100644
index 000000000..15511576c
--- /dev/null
+++ b/packages/fedify/skills/fedify/SKILL.md
@@ -0,0 +1,462 @@
+---
+name: fedify
+description: >-
+ Use this skill whenever writing JavaScript or TypeScript code that uses
+ Fedify to build an ActivityPub server, handle federation activities,
+ implement fediverse features, or integrate Fedify with a web framework
+ such as Hono, Express, Next.js, Nuxt, Fastify, Koa, NestJS, Astro,
+ SvelteKit, Fresh, h3, Elysia, or Cloudflare Workers. Covers the
+ `Federation` builder pattern, actor/inbox/outbox/collection dispatchers,
+ inbox listeners, vocabulary objects from `@fedify/vocab`, key pair
+ management, HTTP Signatures, Object Integrity Proofs, the `KvStore` and
+ `MessageQueue` interfaces, database adapter packages, structured logging
+ with LogTape, OpenTelemetry tracing, the `fedify` CLI toolchain, and
+ common mistakes. Also apply when the user mentions ActivityPub,
+ federation, fediverse, WebFinger, NodeInfo, FEPs, or Mastodon
+ interoperability, even if they do not name Fedify explicitly.
+---
+
+Fedify skill
+============
+
+Fedify is a TypeScript library for ActivityPub server applications. It
+works across Deno, Node.js, and Bun. The library takes care of the fiddly
+parts of the fediverse (HTTP Signatures, Object Integrity Proofs,
+WebFinger, NodeInfo, JSON-LD, delivery queues) so application code can
+stay focused on dispatchers and activity handlers.
+
+Always link into the full documentation at
+instead of guessing. Every docs page is also served as raw Markdown
+by appending `.md` to its path, so
+ returns `text/markdown`.
+This skill uses the `.md` form in every fedify.dev link below so you
+can read the source directly without HTML rendering; when you present
+a link *to the user*, strip the `.md` suffix so browsers render the
+HTML page (so `https://fedify.dev/manual/federation.md` becomes
+`https://fedify.dev/manual/federation`). The index at
+ and the full bundle at
+ are authoritative; this skill only
+points the way. Do not invent APIs; verify names against those docs
+or against the installed `@fedify/fedify` types.
+
+
+Builder pattern
+---------------
+
+Two entry points reach a `Federation` object:
+
+ - `createFederationBuilder()` returns a
+ `FederationBuilder`. Register dispatchers and
+ listeners on it, then `await builder.build(options)` to obtain the
+ `Federation`. Prefer this in larger apps, especially
+ when you need to split configuration across files or avoid circular
+ imports. In serverless runtimes such as Cloudflare Workers,
+ bindings are only available per-request, so the `Federation` must be
+ constructed inside the request handler; the builder pattern is the
+ documented approach there because dispatcher registration can happen
+ at module load time and only the asynchronous `.build(options)` call
+ runs per request.
+ - `createFederation(options)` returns a
+ `Federation` directly. Appropriate when everything
+ fits in one module.
+
+`.build()` is asynchronous; always `await` it. See
+.
+
+~~~~ typescript
+import { createFederationBuilder, MemoryKvStore } from "@fedify/fedify";
+
+const builder = createFederationBuilder();
+// ...register dispatchers on builder...
+export const federation = await builder.build({
+ kv: new MemoryKvStore(), // development only
+});
+~~~~
+
+> [!IMPORTANT]
+> Production deployments *must* provide a real `queue` implementation.
+> Without one, outgoing activities are sent synchronously and delivery
+> becomes unreliable under load. See
+> .
+
+> [!WARNING]
+> Never set `allowPrivateAddress: true` outside tests. It disables the
+> SSRF guard that blocks Fedify from fetching private or loopback
+> addresses. See and
+> .
+
+
+Dispatchers
+-----------
+
+Every route Fedify serves is driven by a dispatcher callback registered on
+the builder (or `Federation` object). Do not hand-roll these routes in
+the web framework; the dispatcher signatures encode the library's URI
+template guarantees.
+
+ - `setActorDispatcher(path, dispatcher)`: returns an
+ `ActorCallbackSetters` chain that also carries
+ `setKeyPairsDispatcher()`.
+ - `setObjectDispatcher(type, path, dispatcher)`: for individual
+ `Object` types such as `Note` or `Article`.
+ - `setInboxDispatcher(path, dispatcher)`: the inbox *collection*
+ endpoint. The inbox *listener* is a different API (see below).
+ - `setOutboxDispatcher(path, dispatcher)`.
+ - `setFollowingDispatcher(path, dispatcher)` /
+ `setFollowersDispatcher(path, dispatcher)` /
+ `setLikedDispatcher(path, dispatcher)` /
+ `setFeaturedDispatcher(path, dispatcher)` /
+ `setFeaturedTagsDispatcher(path, dispatcher)`.
+ - `setCollectionDispatcher()` and `setOrderedCollectionDispatcher()`
+ for custom collections.
+ - `setNodeInfoDispatcher(path, dispatcher)` and
+ `setWebFingerLinksDispatcher(dispatcher)` for protocol endpoints.
+
+Paths use URI templates. If an identifier can contain URI characters,
+switch the template variable from `{identifier}` to `{+identifier}` to
+avoid double-encoding. See .
+
+> [!WARNING]
+> Simple expansion (`{identifier}`) percent-encodes reserved characters a
+> second time. If actors or objects are keyed by URIs, use reserved
+> expansion (`{+identifier}`).
+
+See ,
+, and
+.
+
+
+Inbox listeners
+---------------
+
+`setInboxListeners(inboxPath, sharedInboxPath?)` returns an
+`InboxListenerSetters` object with:
+
+ - `.on(ActivityType, handler)`: chainable, keyed by the *class*
+ (`Follow`, `Create`, `Undo`, etc.).
+ - `.onError(handler)`.
+ - `.onUnverifiedActivity(handler)`.
+ - `.setSharedKeyDispatcher(dispatcher)`.
+ - `.withIdempotency(strategy)`.
+
+> [!WARNING]
+> Activities of a type that is not registered via `.on()` are answered
+> with HTTP 202 and logged at error level as an unsupported activity,
+> but never reach a listener. To catch everything, register a listener
+> for the base `Activity` class.
+
+See .
+
+
+Context and `TContextData`
+--------------------------
+
+`Context` is the per-operation handle Fedify passes to
+dispatchers and listeners. The `TContextData` generic carries
+application state (database handles, request id, auth session). Treat it
+as the single place to inject dependencies; do not reach for module-level
+singletons inside handlers.
+
+`RequestContext` extends `Context` with
+request-scoped helpers.
+
+Use `ctx.get…Uri()` helpers (for example `ctx.getActorUri(identifier)`)
+to build canonical URIs instead of string-concatenating paths.
+
+> [!CAUTION]
+> The `crossOrigin: "trust"` option on context methods and on vocabulary
+> dereferencing disables the same-origin check. Only use it when the
+> remote document is known to be trustworthy; it was the source of
+> prior interop bugs.
+
+See and
+.
+
+
+Framework integrations
+----------------------
+
+Mount Fedify through the dedicated integration package for the target
+framework. Do not translate requests manually; the integration handles
+content negotiation, signature verification, and response streaming.
+
+| Framework | Package |
+| ------------------ | -------------------- |
+| Astro | *@fedify/astro* |
+| Cloudflare Workers | *@fedify/cfworkers* |
+| Elysia | *@fedify/elysia* |
+| Express | *@fedify/express* |
+| Fastify | *@fedify/fastify* |
+| Fresh | *@fedify/fresh* |
+| h3 | *@fedify/h3* |
+| Hono | *@fedify/hono* |
+| Koa | *@fedify/koa* |
+| NestJS | *@fedify/nestjs* |
+| Next.js | *@fedify/next* |
+| Nuxt | *@fedify/nuxt* |
+| SolidStart | *@fedify/solidstart* |
+| SvelteKit | *@fedify/sveltekit* |
+
+Two more packages are frequently useful: *@fedify/debugger* for a local
+ActivityPub dashboard, and *@fedify/relay* for relay implementations.
+
+See .
+
+
+Built-in protocol endpoints
+---------------------------
+
+Fedify serves these endpoints automatically as soon as the federation
+handler is mounted; do not reimplement them.
+
+ - `/.well-known/webfinger` (WebFinger). Customize link output with
+ `setWebFingerLinksDispatcher()`. See
+ .
+ - `/.well-known/nodeinfo` and the versioned NodeInfo document.
+ Customize with `setNodeInfoDispatcher()`. See
+ .
+
+
+Outgoing activities
+-------------------
+
+`ctx.sendActivity(sender, recipients, activity, options?)` is the single
+entry point for outbound delivery. Two overloads:
+
+ - Explicit recipients: pass a single `Recipient` or an array. The
+ `sender` may be a `SenderKeyPair`, a `SenderKeyPair[]`, or
+ `{ identifier }` / `{ username }`.
+ - Fan-out: pass the literal `"followers"` to deliver to the sender's
+ `Followers` collection. In this overload the `sender` must be
+ `{ identifier }` or `{ username }`; a raw `SenderKeyPair` or
+ `SenderKeyPair[]` is rejected because Fedify needs the actor
+ identifier to resolve the followers collection.
+
+Always route outbound activities through the queue in production; this is
+the same `queue` provided to `createFederation()` or `.build()`. Without
+a queue the call blocks until every recipient responds and failed
+deliveries have no retry.
+
+> [!CAUTION]
+> Do not derive an activity's `id` from `(actor, object)`. The same
+> actor can send the same activity shape to the same object more than
+> once (for example `Follow` → `Undo(Follow)` → `Follow` again), and
+> those must be distinct activities. Use a fresh UUID or counter in the
+> fragment.
+
+See .
+
+
+Vocabulary imports
+------------------
+
+Import ActivityStreams and ActivityPub vocabulary types from
+`@fedify/vocab`. The historical path `@fedify/fedify/vocab` is a
+deprecated shim kept for backwards compatibility; new code should not use
+it. Likewise, `@fedify/vocab-runtime` replaces the old
+`@fedify/fedify/runtime` path, and `@fedify/webfinger` replaces the old
+in-tree *src/webfinger*.
+
+> [!CAUTION]
+> Several vocabulary classes collide with JavaScript globals (notably
+> `Object`). When importing, either use a namespace import
+> (`import * as vocab from "@fedify/vocab"`) or alias the individual
+> class.
+
+`fromJsonLd()` and `toJsonLd()` are asynchronous; always `await` them.
+
+> [!WARNING]
+> `crossOrigin: "trust"` on vocabulary deserialization trusts embedded
+> objects without re-fetching. Treat it as you would
+> `dangerouslySetInnerHTML`.
+
+See .
+
+
+Key pair management
+-------------------
+
+`setActorDispatcher(...).setKeyPairsDispatcher(dispatcher)` supplies the
+actor's key pairs. Return *two* keys per actor:
+
+ - An RSA-PKCS#1-v1.5 key for HTTP Signatures (Mastodon interop).
+ - An Ed25519 key for FEP-8b32 Object Integrity Proofs.
+
+Fedify signs outbound activities with whatever keys are available; for
+interop with the widest set of peers, provide both.
+
+> [!WARNING]
+> Private keys must live in secret storage. They are not configuration;
+> do not check them into repositories, embed them in container images,
+> or expose them via admin endpoints.
+
+See .
+
+
+Persistent storage
+------------------
+
+Fedify defines two storage interfaces: `KvStore` (key/value cache and
+idempotence) and `MessageQueue` (delivery plus inbox processing), both
+re-exported from `@fedify/fedify`. Use the built-in `MemoryKvStore` only
+in development or tests.
+
+| Package | `KvStore` | `MessageQueue` |
+| ------------------- | --------- | -------------- |
+| *@fedify/sqlite* | yes | yes |
+| *@fedify/postgres* | yes | yes |
+| *@fedify/mysql* | yes | yes |
+| *@fedify/redis* | yes | yes |
+| *@fedify/amqp* | no | yes |
+| *@fedify/denokv* | yes | yes |
+| *@fedify/cfworkers* | yes | yes |
+
+> [!WARNING]
+> `PostgresMessageQueue` and similar implementations require connection
+> pooling sized for parallel consumers; a single shared connection will
+> deadlock under `ParallelMessageQueue`. See
+> .
+
+> [!WARNING]
+> Do not load-balance worker nodes that drain the queue. Each worker
+> should take traffic independently; putting them behind a load balancer
+> breaks idempotency tracking. See .
+
+See and .
+
+
+Observability
+-------------
+
+### LogTape
+
+Fedify emits structured logs via [LogTape] under the following
+categories. Configure LogTape once at application start (if this
+project has a separate LogTape skill installed, defer to it for the
+generic setup):
+
+ - `fedify.compat.transformers`
+ - `fedify.federation`, `fedify.federation.actor`,
+ `fedify.federation.collection`, `fedify.federation.fanout`,
+ `fedify.federation.http`, `fedify.federation.inbox`,
+ `fedify.federation.outbox`, `fedify.federation.queue`
+ - `fedify.nodeinfo.client`
+ - `fedify.otel.exporter`
+ - `fedify.sig.http`, `fedify.sig.key`, `fedify.sig.ld`,
+ `fedify.sig.proof`
+ - `fedify.utils.docloader`, `fedify.utils.kv-cache`
+ - `fedify.webfinger.server`
+
+> [!CAUTION]
+> Since LogTape 0.7.0, implicit contexts require explicit configuration.
+> See .
+
+[LogTape]: https://logtape.org/
+
+### OpenTelemetry
+
+Pass a `tracerProvider` in `FederationOptions` to have Fedify instrument
+its internals. For trace persistence, `@fedify/fedify/otel` exports
+`FedifySpanExporter`, which writes traces to a `KvStore` so the
+*@fedify/debugger* dashboard can render them.
+
+> [!CAUTION]
+> Initialize the OpenTelemetry SDK *before* importing Fedify. Later
+> registration leaves earlier spans untraced.
+
+See and
+.
+
+
+Looking up FEPs
+---------------
+
+When the user references a Fediverse Enhancement Proposal (for example
+`FEP-8fcf` or `FEP-1b12`), clone the proposals repository locally and
+read the relevant file; Codeberg blocks web scraping and `WebFetch`-style
+requests fail:
+
+~~~~ bash
+git clone https://codeberg.org/fediverse/fep.git
+~~~~
+
+Files are under *fep/* keyed by the four-hex-digit identifier (for
+example *fep/8fcf/fep-8fcf.md*). If the project is configured with the
+[FEP MCP server], prefer that instead.
+
+[FEP MCP server]: https://github.com/dahlia/fep-mcp
+
+
+CLI helpers
+-----------
+
+The `fedify` CLI (distributed as *@fedify/cli*) covers bootstrapping and
+debugging:
+
+ - `fedify init`: scaffold a new project (pick web framework, package
+ manager, KV store, and message queue).
+ - `fedify lookup`: resolve a handle, URL, or WebFinger identifier and
+ print the dereferenced document.
+ - `fedify inbox`: spin up a temporary inbox with a tunnel to inspect
+ incoming activities from real peers.
+ - `fedify webfinger`, `fedify nodeinfo`, `fedify tunnel`,
+ `fedify relay`.
+
+> [!WARNING]
+> `fedify inbox` and `fedify tunnel` are development tools. They open a
+> public tunnel to your local process; do not run them against
+> production data.
+
+See .
+
+
+Common mistakes to avoid
+------------------------
+
+ - Forgetting to `await builder.build(...)` or `await ctx.sendActivity(...)`.
+ Both are asynchronous.
+ - Hand-rolling `/.well-known/webfinger` or `/.well-known/nodeinfo`
+ routes; Fedify already serves them.
+ - Importing from the deprecated shims `@fedify/fedify/vocab` or
+ `@fedify/fedify/runtime`, or from the old in-tree *src/webfinger*
+ path, instead of the dedicated packages `@fedify/vocab`,
+ `@fedify/vocab-runtime`, and `@fedify/webfinger`.
+ - Omitting the `queue` option in production; outgoing delivery becomes
+ synchronous and unreliable.
+ - Running with `MemoryKvStore` in production; it evaporates on every
+ restart.
+ - Running behind a reverse proxy, a tunnel (`fedify tunnel`, ngrok,
+ Cloudflare Tunnel, Tailscale Funnel), or a load balancer without
+ propagating the original origin. Fedify reads `request.url`, so
+ without `X-Forwarded-*` handling it will mint actor IDs and activity
+ URLs using the internal origin (for example `http://localhost:3000`)
+ instead of the public `https://…` address that remote peers
+ dereference. Fix one of two ways: pin
+ `FederationOptions.origin` to the canonical URL, or pipe requests
+ through [x-forwarded-fetch] before they reach Fedify (gated on a
+ `BEHIND_PROXY` flag, since `X-Forwarded-Host` is spoofable from the
+ open internet). See .
+ - Enabling `allowPrivateAddress: true` outside tests; that disables the
+ SSRF guard.
+ - Using `crossOrigin: "trust"` without verifying the remote is
+ actually trusted.
+ - Registering inbox handlers only for specific activity types and
+ expecting delivery-level error handling; unregistered types are
+ answered with HTTP 202 and logged at error level as unsupported,
+ but never reach a listener. Add a catch-all on `Activity` if you
+ need to observe them.
+ - Wiring Fedify into a web framework by writing custom routes instead
+ of importing the matching `@fedify/` package.
+ - Load-balancing queue worker nodes; each worker must take traffic
+ independently.
+ - Using simple URI-template expansion (`{identifier}`) when identifiers
+ contain reserved URI characters; switch to `{+identifier}`.
+ - Deriving an activity's `id` from `(actor, object)`; the same pair
+ can legitimately produce multiple activities of the same shape.
+ - Returning `Tombstone` from an actor dispatcher without checking
+ `RequestContext.getActor({ tombstone: "passthrough" })` semantics;
+ see .
+ - Committing private keys, embedding them in bundles, or exposing them
+ through admin endpoints.
+
+[x-forwarded-fetch]: https://github.com/dahlia/x-forwarded-fetch