@rmdes/indiekit-endpoint-activitypub 3.13.10 → 3.13.12

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,175 @@
1
+ /**
2
+ * Public (federation-facing) route getters, extracted from index.js's
3
+ * `get routesPublic()` + `get contentNegotiationRoutes()` (Phase 2 god-entry split).
4
+ * `self` is the ActivityPubEndpoint instance.
5
+ */
6
+ import express from "express";
7
+
8
+ import { authorizeInteractionController } from "../controllers/authorize-interaction.js";
9
+ import { publicProfileController } from "../controllers/public-profile.js";
10
+ import { jf2ToActivityStreams } from "../jf2-to-as2.js";
11
+
12
+ /**
13
+ * Public routes — Fedify bridge for actor/inbox/collections, plus HTML
14
+ * fallbacks. Mounted at mountPath, in front of the authenticated admin routes.
15
+ * @param {object} self - the ActivityPubEndpoint instance
16
+ * @returns {import("express").Router}
17
+ */
18
+ export function buildRoutesPublic(self) {
19
+ const router = express.Router(); // eslint-disable-line new-cap
20
+
21
+ router.use((req, res, next) => {
22
+ if (!self._fedifyMiddleware) return next();
23
+ // Skip Fedify for admin UI routes — they're handled by the
24
+ // authenticated `routes` getter, not the federation layer.
25
+ if (req.path.startsWith("/admin")) return next();
26
+
27
+ // Fedify's acceptsJsonLd() treats Accept: */* as NOT accepting JSON-LD
28
+ // (it only returns true for explicit application/activity+json etc.).
29
+ // Remote servers fetching actor URLs for HTTP Signature verification
30
+ // (e.g. tags.pub) often omit Accept or use */* — they get HTML back
31
+ // instead of the actor JSON, causing "public key not found" errors.
32
+ // Fix: for GET requests to actor paths, upgrade ambiguous Accept headers
33
+ // to application/activity+json so Fedify serves JSON-LD. Explicit
34
+ // text/html requests (browsers) are unaffected.
35
+ if (req.method === "GET" && /^\/users\/[^/]+\/?$/.test(req.path)) {
36
+ const accept = req.get("accept") || "";
37
+ if (!accept.includes("text/html") && !accept.includes("application/xhtml+xml")) {
38
+ req.headers["accept"] = "application/activity+json";
39
+ }
40
+ }
41
+
42
+ return self._fedifyMiddleware(req, res, next);
43
+ });
44
+
45
+ // Authorize interaction — remote follow / subscribe endpoint.
46
+ // Remote servers redirect users here via the WebFinger subscribe template.
47
+ router.get("/authorize_interaction", authorizeInteractionController(self));
48
+
49
+ // HTML fallback for actor URL — serve a public profile page.
50
+ // Fedify only serves JSON-LD; browsers get 406 and fall through here.
51
+ router.get("/users/:identifier", publicProfileController(self));
52
+
53
+ // Catch-all for federation paths that Fedify didn't handle (e.g. GET
54
+ // on inbox). Without this, they fall through to Indiekit's auth
55
+ // middleware and redirect to the login page.
56
+ router.all("/users/:identifier/inbox", (req, res) => {
57
+ res
58
+ .status(405)
59
+ .set("Allow", "POST")
60
+ .type("application/activity+json")
61
+ .json({
62
+ error: "Method Not Allowed",
63
+ message: "The inbox only accepts POST requests",
64
+ });
65
+ });
66
+ router.all("/inbox", (req, res) => {
67
+ res
68
+ .status(405)
69
+ .set("Allow", "POST")
70
+ .type("application/activity+json")
71
+ .json({
72
+ error: "Method Not Allowed",
73
+ message: "The shared inbox only accepts POST requests",
74
+ });
75
+ });
76
+
77
+ return router;
78
+ }
79
+
80
+ /**
81
+ * Content negotiation — serves AS2 JSON for ActivityPub clients requesting
82
+ * individual post URLs; delegates /nodeinfo/2.1 to Fedify.
83
+ * @param {object} self - the ActivityPubEndpoint instance
84
+ * @returns {import("express").Router}
85
+ */
86
+ export function buildContentNegotiationRoutes(self) {
87
+ const router = express.Router(); // eslint-disable-line new-cap
88
+
89
+ // Let Fedify handle NodeInfo data (/nodeinfo/2.1)
90
+ // Only pass GET/HEAD requests — POST/PUT/DELETE must not go through
91
+ // Fedify here, because fromExpressRequest() consumes the body stream,
92
+ // breaking Express body-parsed routes downstream (e.g. admin forms).
93
+ router.use((req, res, next) => {
94
+ if (!self._fedifyMiddleware) return next();
95
+ if (req.method !== "GET" && req.method !== "HEAD") return next();
96
+ // Only delegate to Fedify for NodeInfo data endpoint (/nodeinfo/2.1).
97
+ // All other paths in this root-mounted router are handled by the
98
+ // content negotiation catch-all below. Passing arbitrary paths like
99
+ // /notes/... to Fedify causes harmless but noisy 404 warnings.
100
+ if (!req.path.startsWith("/nodeinfo/")) return next();
101
+ return self._fedifyMiddleware(req, res, next);
102
+ });
103
+
104
+ // Content negotiation for AP clients on regular URLs
105
+ router.get("{*path}", async (req, res, next) => {
106
+ const accept = req.headers.accept || "";
107
+ const isActivityPub =
108
+ accept.includes("application/activity+json") ||
109
+ accept.includes("application/ld+json");
110
+
111
+ if (!isActivityPub) {
112
+ return next();
113
+ }
114
+
115
+ try {
116
+ // Root URL — redirect to Fedify actor
117
+ if (req.path === "/") {
118
+ const actorPath = `${self.options.mountPath}/users/${self.options.actor.handle}`;
119
+ return res.redirect(actorPath);
120
+ }
121
+
122
+ // Post URLs — look up in database and convert to AS2
123
+ const { application } = req.app.locals;
124
+ const postsCollection = application?.collections?.get("posts");
125
+ if (!postsCollection) {
126
+ return next();
127
+ }
128
+
129
+ const requestUrl = `${self._publicationUrl}${req.path.slice(1)}`;
130
+ const post = await postsCollection.findOne({
131
+ "properties.url": requestUrl,
132
+ });
133
+
134
+ if (!post || post.properties?.deleted) {
135
+ // FEP-4f05: Serve Tombstone for deleted posts
136
+ const { getTombstone } = await import("../storage/tombstones.js");
137
+ const tombstone = await getTombstone(self._collections, requestUrl);
138
+ if (tombstone) {
139
+ res.status(410).set("Content-Type", "application/activity+json").json({
140
+ "@context": "https://www.w3.org/ns/activitystreams",
141
+ type: "Tombstone",
142
+ id: requestUrl,
143
+ formerType: tombstone.formerType,
144
+ published: tombstone.published || undefined,
145
+ deleted: tombstone.deleted,
146
+ });
147
+ return;
148
+ }
149
+ return next();
150
+ }
151
+
152
+ const actorUrl = self._getActorUrl();
153
+ const activity = jf2ToActivityStreams(
154
+ post.properties,
155
+ actorUrl,
156
+ self._publicationUrl,
157
+ { visibility: self.options.defaultVisibility },
158
+ );
159
+
160
+ const object = activity.object || activity;
161
+ res.set("Content-Type", "application/activity+json");
162
+ return res.json({
163
+ "@context": [
164
+ "https://www.w3.org/ns/activitystreams",
165
+ "https://w3id.org/security/v1",
166
+ ],
167
+ ...object,
168
+ });
169
+ } catch {
170
+ return next();
171
+ }
172
+ });
173
+
174
+ return router;
175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "3.13.10",
3
+ "version": "3.13.12",
4
4
  "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
5
5
  "keywords": [
6
6
  "indiekit",