Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 51 additions & 24 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ The workspace is defined by *pnpm-workspace.yaml*; packages live under the

- *packages/drfed* is the main application package. It exports the
`drfed-server` binary from *bin/drfed-server.mjs*.
- *packages/federation* registers the ActivityPub dispatchers and inbox
listeners with Fedify, serializes stored objects and activities, and
owns the instance host rules.
- *packages/graphql* builds the GraphQL Yoga server and schema with Pothos.
- *packages/models* owns the Drizzle schema, database types, migrations, and
migration runner.
Expand All @@ -72,19 +75,22 @@ The workspace is defined by *pnpm-workspace.yaml*; packages live under the
- *packages/models/drizzle* contains generated Drizzle migration files.

Keep package boundaries clear. Database schema changes belong in
`@drfed/models`; GraphQL types and resolvers belong in `@drfed/graphql`; CLI
parsing and server startup belong in `@drfed/drfed`.
`@drfed/models`; ActivityPub handlers and vocabulary serialization belong in
`@drfed/federation`; GraphQL types and resolvers belong in `@drfed/graphql`;
CLI parsing and server startup belong in `@drfed/drfed`. `@drfed/federation`
must not depend on `@drfed/graphql`.


Packages
--------

| Package | npm name | Description |
| ------------------ | ---------------- | ----------------------------------------------- |
| *packages/drfed* | `@drfed/drfed` | CLI binary, server startup, and HTTP serving |
| *packages/graphql* | `@drfed/graphql` | GraphQL schema and Yoga server (Pothos + Relay) |
| *packages/models* | `@drfed/models` | Drizzle schema, relations, and migration runner |
| *packages/web* | `@drfed/web` | SolidStart frontend web app. |
| Package | npm name | Description |
| --------------------- | ------------------- | ----------------------------------------------- |
| *packages/drfed* | `@drfed/drfed` | CLI binary, server startup, and HTTP serving |
| *packages/federation* | `@drfed/federation` | ActivityPub dispatchers, listeners, and origins |
| *packages/graphql* | `@drfed/graphql` | GraphQL schema and Yoga server (Pothos + Relay) |
| *packages/models* | `@drfed/models` | Drizzle schema, relations, and migration runner |
| *packages/web* | `@drfed/web` | SolidStart frontend web app. |

Each package has its own *README.md* with a more detailed breakdown.

Expand Down Expand Up @@ -346,6 +352,38 @@ Review generated SQL before committing it. Drizzle migration files under
installed users.


Federation changes
------------------

The ActivityPub layer lives in *packages/federation*.

- *src/index.ts* exports `buildFederation()`, which creates a fresh Fedify
builder and registers every dispatcher and listener on it, and
`createFederation()`, which builds it with the given Fedify options.
- *src/actor.ts*, *src/object-dispatchers.ts*, *src/collection.ts*, and
*src/inbox.ts* each register one group of handlers on the builder they
are given. Do not register anything on a module-level builder.
- *src/object.ts* holds the query selections and serializers for stored
objects and activities. GraphQL mutations use the same serializers to
build the JSON-LD documents they store, so that stored documents and
ActivityPub responses never diverge.
- *src/origin.ts* holds the instance host rules; see below.

Activity delivery observations live in `activity_deliveries`, independently
of the ActivityPub `activities` resources, and *src/activity-delivery/* holds
the code that records them. The federation HTTP surface must pass through
`createInboundRecorder` with a federation made by `createFederation`, which
tracks the public keys, spans and measurements Fedify reports for each
request, and the deployment's root origin. Keep the public-key cache
serialization compatible with the installed Fedify version,
and read only the spans, events and metrics Fedify documents in its
OpenTelemetry manual; never verify a request again. Inbox listeners must call
`markHandled()`, which is how a delivery tells a received activity from an
acknowledged one. Use `deliverActivity` for outgoing delivery, and create the
federation through `createFederation`, which observes the outbox queue so that
each attempt Fedify's worker makes settles its delivery.


GraphQL changes
---------------

Expand All @@ -362,20 +400,9 @@ When adding a new object or field, follow the existing `builder.drizzleNode()`
and `t.drizzleField()` patterns. Keep resolver database access through
`ctx.db`.

Activity delivery observations live in `activity_deliveries`, independently
of the ActivityPub `activities` resources. The federation HTTP surface must
pass through `createInboundRecorder` with a federation made by
`createFederation`, which tracks the public keys, spans and measurements Fedify
reports for each request, and the deployment's root origin. Keep the
public-key cache serialization compatible with the installed Fedify version,
and read only the spans, events and metrics Fedify documents in its
OpenTelemetry manual; never verify a request again. Inbox listeners must call
`markHandled()`, which is how a delivery tells a received activity from an
acknowledged one. Use `deliverActivity` for outgoing delivery, and create the
federation through `createFederation`, which observes the outbox queue so that
each attempt Fedify's worker makes settles its delivery. Delivery contents are
private to local instance members and administrators, including Relay node
lookups.
The GraphQL types for activity deliveries live in
*src/activity-delivery/entry.ts*. Delivery contents are private to local
instance members and administrators, including Relay node lookups.


CLI and server changes
Expand Down Expand Up @@ -410,8 +437,8 @@ Requests are routed by the authority they arrive on, in
is an instance and serves ActivityPub only; the root origin and every other
authority serve GraphQL and never answer as an instance; anything deeper under
the root domain is answered 421, and an unusable `Host` header 400. The
classification itself lives in *packages/graphql/src/origin.ts* alongside the
functions that compose an instance's authority, so that the two can never
classification itself lives in *packages/federation/src/origin.ts* alongside
the functions that compose an instance's authority, so that the two can never
disagree about what an instance host looks like. Anything that changes how a
host is composed or compared belongs there, not in the server.

Expand Down
2 changes: 1 addition & 1 deletion mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ silent = "stdout"

[tasks."build:server"]
description = "Build the project except web"
run = "pnpm --parallel -F @drfed/graphql -F @drfed/models -F @drfed/drfed build"
run = "pnpm --parallel -F @drfed/federation -F @drfed/graphql -F @drfed/models -F @drfed/drfed build"
silent = "stdout"

[tasks."build:web"]
Expand Down
5 changes: 3 additions & 2 deletions packages/drfed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
============

The main application package for [DrFed], a web-based platform for developing
and debugging ActivityPub apps. It wires together the database layer, GraphQL
server, and HTTP server, and exposes the `drfed-server` CLI binary.
and debugging ActivityPub apps. It wires together the database layer,
ActivityPub federation, GraphQL server, and HTTP server, and exposes the
`drfed-server` CLI binary.

[DrFed]: https://drfed.org/

Expand Down
1 change: 1 addition & 0 deletions packages/drfed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"typescript": "catalog:"
},
"dependencies": {
"@drfed/federation": "workspace:*",
"@drfed/graphql": "workspace:*",
"@drfed/models": "workspace:*",
"@electric-sql/pglite": "catalog:",
Expand Down
4 changes: 1 addition & 3 deletions packages/drfed/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@ import { AsyncLocalStorage } from "node:async_hooks";
import { writeFile } from "node:fs/promises";
import process from "node:process";

import createFederation, { createInboundRecorder } from "@drfed/federation";
import { createYogaServer } from "@drfed/graphql";
import createFederation, {
createInboundRecorder,
} from "@drfed/graphql/federation";
import { schema } from "@drfed/graphql/schema";
import { migrate } from "@drfed/models";
import { PgliteKvStore } from "@fedify/pglite";
Expand Down
4 changes: 1 addition & 3 deletions packages/drfed/src/serving.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ import {
findStrandedInstances,
warnAboutStrandedInstances,
} from "@drfed/drfed/serving";
import createFederation, {
createInboundRecorder,
} from "@drfed/graphql/federation";
import createFederation, { createInboundRecorder } from "@drfed/federation";
import { migrate, relations, schema } from "@drfed/models";
import { uuidV7 as uuid } from "@drfed/models/uuid";
import { PGlite } from "@electric-sql/pglite";
Expand Down
2 changes: 1 addition & 1 deletion packages/drfed/src/serving.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import { canonicalizeAuthority, classifyHost } from "@drfed/graphql/origin";
import { canonicalizeAuthority, classifyHost } from "@drfed/federation/origin";
import type { Database } from "@drfed/models";
import { getLogger } from "@logtape/logtape";

Expand Down
67 changes: 67 additions & 0 deletions packages/federation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@drfed/federation
=================

ActivityPub federation for [DrFed], built with [Fedify]. Registers the
dispatchers and inbox listeners that serve local actors, objects, activities,
and collections, serializes stored objects and activities into ActivityPub
vocabulary, and owns the rules for composing and recognizing instance hosts.

This package depends on `@drfed/models` but not on `@drfed/graphql`, so
serving ActivityPub never requires a GraphQL schema.

[DrFed]: https://drfed.org/
[Fedify]: https://fedify.dev/


Usage
-----

~~~~ ts
import createFederation from "@drfed/federation";

const federation = await createFederation(db, { kv });
~~~~

`createFederation()` creates a fresh builder with every DrFed dispatcher and
listener registered on it, then builds it with the given Fedify options. Each
call returns an independent `Federation` that resolves local actors from the
given database. `buildFederation(db)` returns the builder without building
it, for callers that build it themselves.


Exports
-------

| Import | Contents |
| ------------------------------------- | ------------------------------------------------------------ |
| `@drfed/federation` | `createFederation()` (default), `buildFederation()` |
| `@drfed/federation/activity-delivery` | Inbound recording, outbound delivery, and their observations |
| `@drfed/federation/object` | Query selections, `toObject()`, and `toCreate()` |
| `@drfed/federation/origin` | `instanceHost()`, `classifyHost()`, and other host helpers |

The serializers in `@drfed/federation/object` are the same ones the
dispatchers use, so documents stored by GraphQL mutations match what is served
over ActivityPub. Load rows with the matching selection before serializing
them.


Activity deliveries
-------------------

`createFederation()` tracks the public keys, spans, and measurements Fedify
reports, and observes the outbox queue when one is given, so that each delivery
can be recorded in `activity_deliveries`. Wrap the federation's HTTP surface
with `createInboundRecorder()` to record every inbox request, and send
activities with `deliverActivity()`. Both are also exported from the package
root. The GraphQL fields that read these records are documented in
[`@drfed/graphql`].

[`@drfed/graphql`]: https://github.com/fedify-dev/drfed/tree/main/packages/graphql


Logging
-------

Inbox activity is logged under the `["drfed", "federation"]` category, and
activity delivery recording under
`["drfed", "federation", "activity-delivery"]`.
98 changes: 98 additions & 0 deletions packages/federation/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"name": "@drfed/federation",
"version": "0.1.0",
"description": "ActivityPub federation for DrFed.",
"keywords": [
"ActivityPub",
"fediverse",
"federation",
"debugger"
],
"author": {
"name": "DrFed team",
"url": "https://drfed.org/"
},
"maintainers": [
{
"name": "ChanHaeng Lee",
"email": "2chanhaeng@gmail.com",
"url": "https://chomu.dev/"
},
{
"name": "Hong Minhee",
"email": "hong@minhee.org",
"url": "https://hongminhee.org/"
},
{
"name": "Hyeonseo Kim",
"email": "dodok8@gmail.com",
"url": "https://hackers.pub/@gaebalgom"
},
{
"name": "Jiwon Kwon",
"email": "work@kwonjiwon.org",
"url": "https://kwonjiwon.org/"
}
],
"license": "AGPL-3.0-only",
"engines": {
"node": ">=26.0.0"
},
"type": "module",
"main": "dist/index.mjs",
"types": "dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./activity-delivery": {
"types": "./dist/activity-delivery.d.mts",
"default": "./dist/activity-delivery.mjs"
},
"./object": {
"types": "./dist/object.d.mts",
"default": "./dist/object.mjs"
},
"./origin": {
"types": "./dist/origin.d.mts",
"default": "./dist/origin.mjs"
}
},
"files": [
"dist/",
"README.md"
],
"tsdown": {
"entry": [
"src/index.ts",
"src/activity-delivery.ts",
"src/object.ts",
"src/origin.ts"
],
"dts": {
"sourcemap": true,
"tsconfig": "../../tsconfig.federation.json"
},
"sourcemap": true
},
"scripts": {
"build": "tsdown",
"test": "node --test"
},
"devDependencies": {
"@electric-sql/pglite": "catalog:",
"@logtape/testing-node": "catalog:",
"@types/node": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
},
"dependencies": {
"@drfed/models": "workspace:*",
"@fedify/fedify": "catalog:",
"@fedify/vocab": "catalog:",
"@logtape/logtape": "catalog:",
"@opentelemetry/api": "^1.9.1",
"drizzle-orm": "catalog:"
}
}
40 changes: 40 additions & 0 deletions packages/federation/src/activity-delivery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// DrFed: A web-based platform for developing and debugging ActivityPub apps
// Copyright (C) 2026 DrFed team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

export { createKeyCache } from "./activity-delivery/keycache.ts";
export {
classifyInbound,
createInboundRecorder,
parseBody,
recordedHeaders,
} from "./activity-delivery/inbound.ts";
export { describeActivity } from "./activity-delivery/describe.ts";
export {
declaredKeyId,
hasLdSignature,
proofMethods,
reportedVerdict,
} from "./activity-delivery/verification.ts";
export {
deliverActivity,
groupRecipients,
} from "./activity-delivery/outbound.ts";
export { failureOf, queuedSettlements } from "./activity-delivery/queue.ts";
export type {
ObservedKeyFetch,
ObservedSpan,
TrackedFederation,
} from "./activity-delivery/tracking.ts";
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
} from "./tracking.ts";
import { observeVerification } from "./verification.ts";

const logger = getLogger(["drfed", "graphql", "activity-delivery"]);
const logger = getLogger(["drfed", "federation", "activity-delivery"]);

type InboundStatus = "received" | "acknowledged" | "unverified" | "rejected";

Expand Down
Loading
Loading