CLAUDE.md describes the code after P0
The repo map gains Infrastructure/ and Objects/, the federation invariants are the ones the code now enforces instead of a list of known holes, and Commands, Data, Testing and Deploy cover the test project, the startup order (warm maps, migrate, index), the admin CLI and the deploy checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
d80cd42a0c
commit
1a62e8a567
1 file changed
+67
-31
@@ -73,11 +73,17 @@ PrivaPub/ ASP.NET Core Web API, net10.0
|
||||
Middleware/SocialPubConfigurations.cs every DI registration (auth, federation, services, swagger, CORS)
|
||||
Controllers/ClientToServer/ /clientapi/*: RootUser (signup/login/invitations/recovery), PrivateAvatar,
|
||||
Group, Post (posts + DMs), Admin, Data
|
||||
Infrastructure/
|
||||
Http/ FederationHttp + SafeHttpHandlerFactory + IpRangeGuard: the only way out
|
||||
Data/ Indexes (created at start), EntityMaps.Warm, Migrations/_NNN_*.cs
|
||||
Cli/ AdminCommands (`PrivaPub admin promote|demote <root>`)
|
||||
RateLimiting.cs accounts (per client address) and inbox (per sending origin) policies
|
||||
Federation/
|
||||
Controllers/ PeasantsController (actor, outbox, followers, following, posts, inboxes),
|
||||
WellKnownController (webfinger, nodeinfo), UsersController (redirect)
|
||||
Actors/ LocalActorService (LocalActor, Keys), RemoteActorService (signed fetch,
|
||||
WebFinger, ForeignAvatar cache)
|
||||
Actors/ LocalActorService (LocalActor, Keys, ReservedName), RemoteActorService
|
||||
(authoritative fetch, key verification, WebFinger), ActorDocument (parser)
|
||||
Objects/ Origin (same-origin rules), ContentSanitizer (HtmlSanitizer, Mastodon allowlist)
|
||||
Signing/ HttpSignatures (draft-cavage sign/verify)
|
||||
Inbox/ InboxService (Follow/Undo/Create/Delete/Update)
|
||||
Outbox/ DeliveryService + DeliveryWorker
|
||||
@@ -87,6 +93,7 @@ PrivaPub/ ASP.NET Core Web API, net10.0
|
||||
StaticServices/ DbEntities (Find<T> accessors), AuthTokenManager (JWT), PasswordHasher
|
||||
Data/InitDb.cs first-run seeding (languages)
|
||||
PrivaPub.ClientModels/ DTOs + validation resources shared with clients
|
||||
PrivaPub.Tests/ xUnit v3; Support/ has a fake two-origin peer and a throwaway-database fixture
|
||||
deploy/ nginx vhost, systemd units (privapub, privapub-mongod), max/setup.sh
|
||||
.gitea/workflows/ build.yml (push) · deploy.yml (tag v*)
|
||||
docs/ROADMAP.md decisions + phased plan
|
||||
@@ -99,6 +106,9 @@ one pure-move commit at a time.
|
||||
|
||||
```bash
|
||||
dotnet build PrivaPub.sln -c Release
|
||||
dotnet test PrivaPub.sln # unit tests; integration tests skip
|
||||
PRIVAPUB_TEST_MONGOD=1 dotnet test PrivaPub.sln # all of them, against mongod on 127.0.0.1:27017
|
||||
# (PRIVAPUB_TEST_MONGO overrides; a fresh database per run, dropped after)
|
||||
cd PrivaPub && ASPNETCORE_ENVIRONMENT=Development \
|
||||
Kestrel__Endpoints__Http__Url=http://127.0.0.1:6970 Kestrel__Endpoints__Http__Protocols=Http1AndHttp2 \
|
||||
AppConfiguration__BackendBaseAddress=http://127.0.0.1:6970 MongoSettings__Database=PrivaPubTest \
|
||||
@@ -106,42 +116,58 @@ cd PrivaPub && ASPNETCORE_ENVIRONMENT=Development \
|
||||
```
|
||||
|
||||
Development config (`appsettings.Development.json`) binds HTTPS 7195 with HTTP/2 only; the overrides above make it
|
||||
curl-able. Outbound fetches only go to https DNS hosts, so local federation needs a real peer (see Testing).
|
||||
curl-able. Outbound fetches only go to https DNS names resolving to public addresses; a test network (Pasture) sets
|
||||
`Federation__AllowPrivateNetworks=true` and `Federation__AllowPlainHttp=true`, which startup refuses in Production.
|
||||
|
||||
Promoting an admin on the box (signing up as "admin" grants nothing):
|
||||
|
||||
```bash
|
||||
cd /var/www/privapub.thepra.dev && sudo -u www-data ASPNETCORE_ENVIRONMENT=Production ./PrivaPub admin promote <root>
|
||||
```
|
||||
|
||||
## Federation invariants
|
||||
|
||||
1. **Every outbound request is signed** with draft-cavage rsa-sha256 over `(request-target) host date` (+ `digest` on
|
||||
bodies). GoToSocial and authorized-fetch Mastodon require signed GETs.
|
||||
2. **Inbound inboxes verify everything before acting:**
|
||||
- the Digest matches the body;
|
||||
- the Date is inside the window;
|
||||
- the signature verifies against the key owner's key;
|
||||
- the activity's `actor` equals the key owner.
|
||||
3. **Status codes:**
|
||||
1. **Every outbound request goes through `IFederationHttp`.** Its handler resolves the name itself and connects only
|
||||
to public addresses; redirects are followed by hand (three at most, each re-checked); bodies are capped at 1 MB;
|
||||
only JSON media types are read; a refused URL is not asked again for five minutes. Never create another
|
||||
`HttpClient` for federation.
|
||||
2. **Every fetch is signed by the instance actor** (`privapub`), never by a persona; deliveries are signed by the acting
|
||||
avatar or group. Both are draft-cavage rsa-sha256 over `(request-target) host date` (+ `digest` on bodies).
|
||||
3. **A remote document is believed only from its own address.** `RemoteActorService.FetchObject` requires the
|
||||
document's `id` to be the URL it was served from (a same-origin alias is followed once). A key is accepted only if
|
||||
the actor lists it, its `owner` is the actor and it shares the actor's origin.
|
||||
4. **Inbound inboxes verify everything before acting:**
|
||||
- the signature covers `(request-target)`, `host`, `digest` and `date` or `(created)`;
|
||||
- the Digest matches the body and the date is at most an hour old and fifteen minutes ahead;
|
||||
- the signature verifies against the key owner's key, and the activity's `actor` is the key owner;
|
||||
- the activity's `id`, and any object it creates, updates or deletes, is on the actor's origin; a cross-origin
|
||||
object is refetched from its own origin.
|
||||
5. **Status codes:**
|
||||
- bad or missing signature: **401**;
|
||||
- malformed body: **400**;
|
||||
- malformed or forged body: **400**;
|
||||
- accepted: **202**;
|
||||
- over the rate limit: **429**;
|
||||
- **never 500** from `/peasants` or an inbox. Peers retry or "double-knock" based on these codes.
|
||||
4. **Actor documents:**
|
||||
6. **Actor documents:**
|
||||
- served as `application/activity+json`;
|
||||
- `publicKey` is an SPKI PEM at `{actor}#main-key`, with `owner` equal to the actor's id;
|
||||
- the shared inbox is advertised in `endpoints.sharedInbox`.
|
||||
5. **Delete and Update apply only to objects the sender authored.** A group re-announces only members' posts.
|
||||
6. **Durable delivery:** activities go out through the delivery queue, never inline in a request, and are signed by
|
||||
the acting avatar or group.
|
||||
7. **Known weaknesses being fixed in roadmap phase P0:**
|
||||
- an actor's document isn't yet checked against its fetch URL (key-cache poisoning);
|
||||
- the SSRF guard is string-only;
|
||||
- remote HTML is stored unsanitised;
|
||||
- object origins aren't checked.
|
||||
|
||||
Don't build on these behaviours.
|
||||
7. **Remote HTML is sanitized before it is stored** (`ContentSanitizer`); `Post.ContentHtml` is what is shown,
|
||||
`ContentFormat` says what `Text` holds. Remote names are plain text.
|
||||
8. **Circles never federate.** A circle's actor, collections, WebFinger and inbox answer 404, and its posts are
|
||||
`IsLocalOnly`. Only communities are Group actors.
|
||||
9. **A DM joins a conversation only by `DmGroup.ParticipantsKey`**, the exact set of its participants; a remote
|
||||
`context` decides nothing.
|
||||
10. **Durable delivery:** activities go out through the delivery queue, never inline in a request.
|
||||
|
||||
## Privacy invariants
|
||||
|
||||
- **No root id in federation output, NodeInfo or logs, and no IP next to an identity in logs.** Some log lines in
|
||||
`RootUserController` still do this; P0 removes them.
|
||||
- **Never derive a public name from the root username.** Invitation sign-up still does; P0 fixes it.
|
||||
- **No root id in federation output, NodeInfo or logs, no IP next to an identity in logs, and no `ex.Message` to a
|
||||
client** ("Something went wrong." instead).
|
||||
- **Never derive a public name from the root username.** Invitation sign-up takes `AvatarUserName` and refuses one
|
||||
equal to the login.
|
||||
- **One username space:** personas, groups and the instance reserve their name in `ReservedName` (unique index) before
|
||||
they are saved; `LocalActorService.TryReserveUserName` is the only way to claim one.
|
||||
- **Per-avatar state stays per avatar:** blocks, mutes, notifications, follows. Nothing may relate sibling avatars.
|
||||
- **Location-ranged posts never federate.**
|
||||
|
||||
@@ -153,6 +179,10 @@ curl-able. Outbound fetches only go to https DNS hosts, so local federation need
|
||||
- **Collection name = class name, so never rename an entity class.**
|
||||
- `Entity.ID` is a 24-character lowercase hex string; `GenerateNewID()` returns `object`, so cast it.
|
||||
- New fields must be additive: a deploy rollback restores the binary, not the database.
|
||||
- **Startup order:** `EntityMaps.Warm()` (every entity's class map, one at a time; two mapped at once throw "An item
|
||||
with the same key has already been added" and stay broken), then `MigrateAsync` (`Infrastructure/Data/Migrations`,
|
||||
`_NNN_` order, each runs once), then `Indexes.Create()`. A new entity needs nothing; a new unique index needs a
|
||||
dedupe migration before it.
|
||||
- Production runs its **own mongod** on 127.0.0.1:27022 (unit `privapub-mongod`, no auth, data in
|
||||
`/var/lib/privapub/mongo`). The box's shared mongod needs credentials nobody here has.
|
||||
|
||||
@@ -173,9 +203,14 @@ curl-able. Outbound fetches only go to https DNS hosts, so local federation need
|
||||
|
||||
## Testing
|
||||
|
||||
There is no test project yet; roadmap P0 adds `PrivaPub.Tests` (xUnit v3) with fixture JSON captured per peer.
|
||||
`PrivaPub.Tests` (xUnit v3). Unit tests need nothing; tests marked `Category=Integration` need a mongod and skip
|
||||
without `PRIVAPUB_TEST_MONGOD=1`. CI runs the unit tests only (the box's mongods are production).
|
||||
|
||||
Until then, verify by building, running locally, and exercising:
|
||||
- `Support/Peer` is an in-process HTTP server answering on two origins (`127.0.0.1` and `localhost`), so origin rules
|
||||
can be tested; `Support/RemoteActor` signs real deliveries with its own key.
|
||||
- Inbox scenarios go through `InboxService.Receive` with a signed request, not through the private handlers.
|
||||
|
||||
Beyond the tests, verify by building, running locally, and exercising:
|
||||
- the client API (sign up, create an avatar, a group, a post);
|
||||
- the ActivityPub endpoints with curl and `Accept: application/activity+json`.
|
||||
|
||||
@@ -184,9 +219,10 @@ the owner's GoToSocial at social.arasaka.software. **Ask before acting from the
|
||||
|
||||
## Deploy
|
||||
|
||||
- **CI/CD:** push to `master` runs `build.yml` on the instance-wide `build` runner. A `v*` tag runs `deploy.yml`:
|
||||
self-contained linux-x64 publish, snapshot to `/var/backups/privapub.thepra.dev`, stop → rsync → start, a
|
||||
`127.0.0.1:6970/build.json` health loop with rollback, then public checks.
|
||||
- **CI/CD:** push to `master` runs `build.yml` (build + tests) on the instance-wide `build` runner. A `v*` tag runs
|
||||
`deploy.yml`: tests, self-contained linux-x64 publish, snapshot and `mongodump` to `/var/backups/privapub.thepra.dev`,
|
||||
stop → rsync → start, a `127.0.0.1:6970/build.json` health loop with rollback, then public checks (actor, NodeInfo,
|
||||
Swagger 404, inbox junk 400, unsigned 401).
|
||||
- **The box:** Max (`nuvola.xyz`). Unit `privapub` runs as www-data from `/var/www/privapub.thepra.dev` with
|
||||
`ASPNETCORE_ENVIRONMENT=Production`.
|
||||
- **One-time root setup:** `deploy/max/setup.sh`, run through `../arasaka.software/tools/max/run.sh`.
|
||||
|
||||
Reference in new issue
Block a user