social-federation tasks 3.1–3.4, 4.1–4.8. With this, a Mastodon user can follow a public trails user: WebFinger → actor fetch → signed Follow → recorded + Accept(Follow) pushed back. Identity surface (section 3): - Actor objects now carry the user's public key (publicKey + assertionMethods via Fedify key pairs dispatcher; keys generated lazily as a fallback to the backfill) and an inbox IRI; url uses localActorIri (3.1). - Software discovery shipped as standard NodeInfo (/.well-known/nodeinfo + /nodeinfo/2.1, software.name trails-cool) instead of the originally-sketched custom AS actor field — Fedify's typed vocab can't emit arbitrary actor props and NodeInfo is what the fediverse reads. Artifacts updated accordingly (3.4). Inbox (section 4): - /users/:username/inbox resource route; HTTP Signatures verified by Fedify before any listener runs (4.1). Rate-limited 60 req/5 min per source instance (host from Signature keyId) BEFORE verification so hostile instances can't burn CPU on key fetches (4.8). - Listeners: Follow → auto-accept for public profiles + Accept pushed back (4.2); Undo(Follow) → row removed (4.3); Accept(Follow) → Pending settled + first outbox poll enqueued (4.4); Reject(Follow) → Pending dropped (4.5; UI notice deferred to 6.6). Unhandled types are acknowledged + dropped by Fedify (4.6). - Replay protection via Fedify's KvStore, now Postgres-backed (journal.federation_kv + daily sweep job) so dedupe survives restarts (4.7). Schema (discovered requirement): - follows.follower_id relaxed to nullable + follows.follower_actor_iri for inbound remote followers — the proposal's 'follows is already federation-ready' only held for outbound. Check constraint enforces exactly one follower identity; partial unique index dedupes remote follows. Notification fan-out + approve flow now filter local followers explicitly. Design/proposal updated. Tests: 7 inbox integration tests (accept/refuse/idempotence/undo/ settle/reject/check-constraint), 6 KvStore integration tests, 2 unit tests for source-host extraction. All against real Postgres, gated on FEDERATION_INTEGRATION=1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
35 lines
1.3 KiB
TypeScript
35 lines
1.3 KiB
TypeScript
import type { Route } from "./+types/users.$username.inbox";
|
|
import { handleFederationRequest, federationSourceHost } from "~/lib/federation.server";
|
|
import { consumeRateLimit } from "~/lib/rate-limit.server";
|
|
|
|
/**
|
|
* POST /users/:username/inbox — ActivityPub inbox (spec:
|
|
* social-federation, "Narrow inbox"). Fedify verifies the HTTP
|
|
* Signature and dispatches to the registered listeners; everything
|
|
* else about the request is its problem. We rate-limit per source
|
|
* instance *before* signature verification so a hostile instance
|
|
* can't burn CPU on key fetches + crypto (spec 4.8: 60 req / 5 min
|
|
* per host).
|
|
*/
|
|
export function action({ request }: Route.ActionArgs) {
|
|
const { allowed, resetMs } = consumeRateLimit({
|
|
scope: "federation-inbox",
|
|
key: federationSourceHost(request),
|
|
limit: 60,
|
|
windowMs: 5 * 60 * 1000,
|
|
});
|
|
if (!allowed) {
|
|
return Promise.resolve(
|
|
new Response("Too Many Requests", {
|
|
status: 429,
|
|
headers: { "Retry-After": String(Math.ceil(resetMs / 1000)) },
|
|
}),
|
|
);
|
|
}
|
|
return handleFederationRequest(request);
|
|
}
|
|
|
|
/** Inboxes are POST-only; GET has no representation here. */
|
|
export function loader() {
|
|
return new Response("Method Not Allowed", { status: 405, headers: { Allow: "POST" } });
|
|
}
|