Package Exports
- @dwk/activitypub
Readme
@dwk/activitypub
Edge-native ActivityPub actor: inbox/outbox, follower collections, signed server-to-server federation. Endpoint package + Durable Object.
Part of the @dwk IndieWeb + Solid cohort. See the
package specification for the full
requirements.
A native ActivityPub actor rooted at the
user's own domain — making the self-owned presence a first-class fediverse
citizen (followers, replies, boosts) rather than a bridged guest. It mirrors the
architecture proven in @dwk/solid-pod: a stateless
front door (routing + edge HTTP-signature verification) over a per-actor
Durable Object that is the consistency authority for activity-id dedup, the
follower/following/outbox collections, and the signed outbound delivery queue.
This is the second @dwk package to ship a Durable Object.
What v1 covers
- Actor document (
Person) served asapplication/activity+json, with the public key embedded inline so peers can verify this actor's signatures. - Collections —
outbox,followers,followingas pagedOrderedCollections;inboxis write-only to peers. - Server-to-server (inbound)
POST /inbox: HTTP-signature verification at the edge, dedup by activityid, and handling ofFollow/Undo/Accept/Create/Update/Like/Announce/Delete. - Server-to-server (outbound): auto-
Acceptof follows and signed fan-out delivery to follower inboxes, with retry/backoff driven by DO alarms. - NodeInfo — the
/.well-known/nodeinfodiscovery document and a mostly-staticnodeinfo/2.1document (liveusagecounts pulled from the DO). - Owner publish endpoint (
POST <actor>/outbox, bearer-token gated) — the publish →Createfan-out seam for@dwk/micropub. Full client-to-server authoring is out of scope for v1.
Usage
import { createActivityPub, ActivityPubObject } from "@dwk/activitypub";
const activitypub = createActivityPub({
baseUrl: "https://example.com",
actor: { username: "alice", name: "Alice", summary: "Hello, fediverse." },
publicKeyPem: env.AP_PUBLIC_KEY, // published in the actor document
privateKeyPem: env.AP_PRIVATE_KEY, // signs outbound deliveries (secret binding)
publishToken: env.AP_PUBLISH_TOKEN, // optional: enables POST <actor>/outbox
software: { name: "anglesite", version: "1.2.3" }, // NodeInfo
});
// In your Worker's fetch handler:
// GET /users/alice → actor document
// GET /users/alice/{outbox,followers,following}[?page=N]
// POST /users/alice/inbox → signed S2S delivery
// GET /.well-known/nodeinfo, /nodeinfo/2.1
return activitypub(request, env, ctx);
// Bind the Durable Object the package ships:
export { ActivityPubObject };// wrangler.jsonc — the binding the package declares
{
"durable_objects": {
"bindings": [{ "name": "ACTOR", "class_name": "ActivityPubObject" }],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ActivityPubObject"] }],
}The actor IRI and every collection IRI are derived from baseUrl + username
(https://example.com/users/alice, …/inbox, …#main-key). Mount the package
under a path prefix by including it in baseUrl (e.g.
https://example.com/ap) — the handler routes purely on the request URL.
Bindings (declared Env fragment)
ACTOR— the Durable Object namespace for the per-actor class (ActivityPubObject). The single authoritative store for dedup, collections, and the delivery queue. The handler fails loudly at startup if it is missing.
No KV is used for any authoritative state, per
spec/non-functional-requirements.md:
follower lists, dedup, and the delivery queue all live in the DO's SQLite.
HTTP signatures
Inbound POST /inbox deliveries are authenticated, and outbound deliveries are
signed, with the de-facto fediverse draft-cavage-http-signatures profile
(RSA-SHA256 over a covered header set, body integrity via Digest). The
implementation is RSA-only with an explicit algorithm allow-list (no none, no
symmetric algorithms), mirroring the @dwk/dpop hardening
posture.
Signing/verification sit behind the verifyInboxSignature config seam, so the
forthcoming cross-standard @dwk/http-signatures package (RFC 9421 +
draft-cavage; #59) can be
swapped in unchanged once it lands.
Delivery safety
Outbound deliveries target attacker-influenced URLs (a follower's advertised
inbox), so every target passes a syntactic SSRF guard before any request leaves:
HTTPS only, and private / loopback / link-local / cloud-metadata hosts are
refused. (DNS rebinding is out of scope — the Workers runtime does not expose
name resolution to user code; same limitation as
@dwk/webmention's safe-fetch.) A 4xx from a peer
is a permanent failure (dropped); 5xx, 408, 429, and network errors are
retried with exponential backoff up to deliveryMaxAttempts.
Design
The front door is stateless and serves the static actor + NodeInfo documents
directly; everything that touches authoritative state is routed to the per-actor
Durable Object. Per the composition contract, the actor profile, key material,
and delivery policy are all passed into createActivityPub — nothing is read
from the global environment — so an actor can be instantiated multiple times and
tested in isolation.
ActivityStreams 2.0 documents are emitted in the compact JSON-LD form the
fediverse interoperates over (Mastodon et al.), with the AS2 + security
@context. Full RDF content-negotiation via @dwk/rdf is a
future enhancement (its v1 JSON-LD subset and the AS2 context are tracked in
spec/open-questions.md §4).
Observability
Federation events flow through the injected @dwk/log Logger/Metrics seams
(default no-op): activitypub.signature.{accepted,rejected},
activitypub.inbox.{accepted,duplicate},
activitypub.delivery.{succeeded,failed,blocked}, and
activitypub.publish.rejected. Per the redaction policy, only reason codes,
activity types, and sanitized hosts are recorded — never key material, tokens,
or bodies.