@oxy.so/federation 2.7.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A DELETED local actor, as the fediverse is told about it.
3
+ *
4
+ * Two halves, following Mastodon's account deletion:
5
+ *
6
+ * 1. Once, the app sends `Delete{actor}`: the actor deletes itself, signed with
7
+ * its own key, to every follower's (shared) inbox. That is
8
+ * `DeliveryService.federateActorDelete`, built by {@link buildActorDeleteActivity}.
9
+ * 2. From then on the actor's URLs answer `410 Gone` instead of `404`. That is
10
+ * what a server that missed the Delete, or never acts on one, reads as
11
+ * permanent: Mastodon deletes its copy of an account whose WebFinger answers
12
+ * 410, and Lemmy, PieFed and Mbin mark a community or person deleted when a
13
+ * refetch answers 410 or a `Tombstone`. A 404 tells all of them nothing, so
14
+ * they keep the ghost profile and keep delivering to it.
15
+ *
16
+ * The routers ask the app's `isActorGone(username)` only after `resolveUser`
17
+ * found nobody, so a live account is never answered as gone.
18
+ */
19
+
20
+ import { AP_CONTEXT } from '../apContext';
21
+
22
+ /** Does `username` name a local actor that was deleted? Asked only for a username that resolved to nobody. */
23
+ export type IsActorGone = (username: string) => Promise<boolean>;
24
+
25
+ /** The body of a `410 Gone` for a deleted object: a bare `Tombstone` naming it. */
26
+ export function goneTombstone(id: string): Record<string, unknown> {
27
+ return { '@context': AP_CONTEXT, id, type: 'Tombstone' };
28
+ }
29
+
30
+ /**
31
+ * The actor's `Delete` of itself. The id is DETERMINISTIC (no timestamp), the
32
+ * shape Mastodon sends (`<actor>#delete`), so a retried run re-sends the same
33
+ * activity and a delivery queue that dedupes on activity id + inbox sends each
34
+ * inbox one copy.
35
+ */
36
+ export function buildActorDeleteActivity(actorUri: string): Record<string, unknown> {
37
+ return {
38
+ '@context': AP_CONTEXT,
39
+ id: `${actorUri}#delete`,
40
+ type: 'Delete',
41
+ actor: actorUri,
42
+ to: ['https://www.w3.org/ns/activitystreams#Public'],
43
+ object: actorUri,
44
+ };
45
+ }
package/src/node/index.ts CHANGED
@@ -141,6 +141,12 @@ export {
141
141
  * The ActivityPub actor + inbox + follow-graph router (actor GET incl. the
142
142
  * `instance` actor, inbox POST with HTTP-sig verify, followers/following pages).
143
143
  */
144
+ /**
145
+ * A deleted local actor: its `Delete` of itself, and the `Tombstone` its URLs
146
+ * answer `410 Gone` with afterwards.
147
+ */
148
+ export { buildActorDeleteActivity, goneTombstone, type IsActorGone } from './goneActor';
149
+
144
150
  export {
145
151
  createActorRouter,
146
152
  type ActorRouterConfig,
@@ -17,6 +17,7 @@ import { discoveryCacheControl } from './httpCacheControl';
17
17
  import { isSameFederationHost } from '../apUri';
18
18
  import type { UrlBuilders } from '../urls';
19
19
  import { INSTANCE_ACTOR_USERNAME, normalizeActorUsername } from '../urls';
20
+ import type { IsActorGone } from './goneActor';
20
21
 
21
22
  /** Default HTTP response max-age. The injected JRD cache has its own policy. */
22
23
  const WEBFINGER_CACHE_TTL = 3600;
@@ -65,6 +66,12 @@ export interface WebfingerRouterConfig {
65
66
  get(username: string): Promise<WebfingerJrd | null>;
66
67
  set(username: string, jrd: WebfingerJrd): void;
67
68
  };
69
+ /**
70
+ * Whether a username that resolved to nobody names a DELETED local actor
71
+ * (`goneActor.ts`). True ⇒ `410 Gone` instead of 404, which is what makes
72
+ * Mastodon delete its copy of the account. Omitted ⇒ every miss is a 404.
73
+ */
74
+ isActorGone?: IsActorGone;
68
75
  /** HTTP response TTL in whole seconds, 0..86400. Defaults to 3600; 0 sends no-store. */
69
76
  httpCacheTtlSeconds?: number;
70
77
  /** Diagnostics sink. */
@@ -147,7 +154,13 @@ export function createWebfingerRouter(config: WebfingerRouterConfig): Router {
147
154
  }
148
155
 
149
156
  const user = await config.resolveUser(username);
150
- if (!user) return res.status(404).json({ error: 'User not found' });
157
+ if (!user) {
158
+ if (config.isActorGone && (await config.isActorGone(username))) {
159
+ res.set('Cache-Control', httpCacheControl);
160
+ return res.status(410).json({ error: 'User deleted' });
161
+ }
162
+ return res.status(404).json({ error: 'User not found' });
163
+ }
151
164
 
152
165
  // Sharing OFF must be indistinguishable from a nonexistent user — same 404
153
166
  // body, no separate error code. UNLIKE the other user-scoped surfaces,