@oxy.so/federation 2.3.0 → 2.4.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.
- package/README.md +31 -0
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/node/actorRouter.js +4 -2
- package/dist/cjs/node/delivery.js +28 -6
- package/dist/cjs/node/httpCacheControl.js +11 -0
- package/dist/cjs/node/index.js +2 -1
- package/dist/cjs/node/webfingerRouter.js +6 -4
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/node/actorRouter.js +4 -2
- package/dist/esm/node/delivery.js +26 -6
- package/dist/esm/node/httpCacheControl.js +8 -0
- package/dist/esm/node/index.js +1 -1
- package/dist/esm/node/webfingerRouter.js +6 -4
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/node/actorRouter.d.ts +2 -0
- package/dist/types/node/delivery.d.ts +21 -1
- package/dist/types/node/httpCacheControl.d.ts +2 -0
- package/dist/types/node/index.d.ts +1 -1
- package/dist/types/node/webfingerRouter.d.ts +2 -0
- package/package.json +1 -1
- package/src/__tests__/delivery.test.ts +109 -1
- package/src/__tests__/routers.test.ts +70 -0
- package/src/node/actorRouter.ts +6 -2
- package/src/node/delivery.ts +57 -9
- package/src/node/httpCacheControl.ts +8 -0
- package/src/node/index.ts +2 -0
- package/src/node/webfingerRouter.ts +8 -4
|
@@ -23,6 +23,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
23
23
|
exports.createActorRouter = createActorRouter;
|
|
24
24
|
const express_1 = require("express");
|
|
25
25
|
const apContext_1 = require("../apContext");
|
|
26
|
+
const httpCacheControl_1 = require("./httpCacheControl");
|
|
26
27
|
const httpSignature_1 = require("../httpSignature");
|
|
27
28
|
const urls_1 = require("../urls");
|
|
28
29
|
/** Page size for the paginated followers/following collections (mirrors the outbox). */
|
|
@@ -57,6 +58,7 @@ function parseFollowOffset(raw) {
|
|
|
57
58
|
function createActorRouter(config) {
|
|
58
59
|
const router = (0, express_1.Router)();
|
|
59
60
|
const { urls, domain, apContentType, logger } = config;
|
|
61
|
+
const httpCacheControl = (0, httpCacheControl_1.discoveryCacheControl)(config.httpCacheTtlSeconds, 1800);
|
|
60
62
|
function wantsActivityPub(req) {
|
|
61
63
|
return config.wantsActivityPub(req.headers.accept);
|
|
62
64
|
}
|
|
@@ -242,7 +244,7 @@ function createActorRouter(config) {
|
|
|
242
244
|
},
|
|
243
245
|
};
|
|
244
246
|
res.set('Content-Type', apContentType);
|
|
245
|
-
res.set('Cache-Control',
|
|
247
|
+
res.set('Cache-Control', httpCacheControl);
|
|
246
248
|
return res.json(actorObject);
|
|
247
249
|
}
|
|
248
250
|
const user = await config.resolveUser(username);
|
|
@@ -277,7 +279,7 @@ function createActorRouter(config) {
|
|
|
277
279
|
alsoKnownAs: user.alsoKnownAs,
|
|
278
280
|
});
|
|
279
281
|
res.set('Content-Type', apContentType);
|
|
280
|
-
res.set('Cache-Control',
|
|
282
|
+
res.set('Cache-Control', httpCacheControl);
|
|
281
283
|
return res.json({ '@context': apContext_1.AP_CONTEXT, ...actorObject });
|
|
282
284
|
}
|
|
283
285
|
catch (err) {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* call `deliverToFollowers` / `deliverActivity` / `queueDelivery` here.
|
|
26
26
|
*/
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.DeliveryAuthorizationDeniedError = void 0;
|
|
28
29
|
exports.createDeliveryService = createDeliveryService;
|
|
29
30
|
const apContext_1 = require("../apContext");
|
|
30
31
|
const httpSignature_1 = require("../httpSignature");
|
|
@@ -34,6 +35,15 @@ const DELIVER_ACTIVITY_TIMEOUT_MS = 15000;
|
|
|
34
35
|
const DELIVERY_RESPONSE_PREVIEW_MAX_BYTES = 1024;
|
|
35
36
|
/** The ActivityStreams public collection — the `to` addressee of a public activity. */
|
|
36
37
|
const AP_PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
|
|
38
|
+
/** A definitive refusal: workers must discard, not retry or count a delivery. */
|
|
39
|
+
class DeliveryAuthorizationDeniedError extends Error {
|
|
40
|
+
constructor() {
|
|
41
|
+
super('Outbound federation delivery is not authorized');
|
|
42
|
+
this.code = 'DELIVERY_AUTHORIZATION_DENIED';
|
|
43
|
+
this.name = 'DeliveryAuthorizationDeniedError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.DeliveryAuthorizationDeniedError = DeliveryAuthorizationDeniedError;
|
|
37
47
|
/** Read a bounded prefix of a failed-delivery response body for the debug log. */
|
|
38
48
|
async function readResponsePreview(response) {
|
|
39
49
|
const chunks = [];
|
|
@@ -72,7 +82,17 @@ function createDeliveryService(config) {
|
|
|
72
82
|
const host = urlHost(url);
|
|
73
83
|
return host === null || config.isBlockedDomain(host);
|
|
74
84
|
}
|
|
85
|
+
async function authorizeDelivery(activity, senderAccountId) {
|
|
86
|
+
if (config.authorizeDelivery &&
|
|
87
|
+
!(await config.authorizeDelivery({ activity, senderAccountId }))) {
|
|
88
|
+
throw new DeliveryAuthorizationDeniedError();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
75
91
|
async function deliverActivity(activity, targetInbox, senderOxyUserId, senderUsername) {
|
|
92
|
+
await authorizeDelivery(activity, senderOxyUserId);
|
|
93
|
+
return deliverAuthorizedActivity(activity, targetInbox, senderUsername);
|
|
94
|
+
}
|
|
95
|
+
async function deliverAuthorizedActivity(activity, targetInbox, senderUsername) {
|
|
76
96
|
if (isBlockedUrl(targetInbox)) {
|
|
77
97
|
logger.warn(`[FedDeliver] refusing outbound delivery to blocked inbox ${targetInbox}`);
|
|
78
98
|
return false;
|
|
@@ -110,6 +130,7 @@ function createDeliveryService(config) {
|
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
async function queueDelivery(activity, targetInbox, senderOxyUserId) {
|
|
133
|
+
await authorizeDelivery(activity, senderOxyUserId);
|
|
113
134
|
if (isBlockedUrl(targetInbox)) {
|
|
114
135
|
logger.warn(`[FedDeliver] not queueing delivery to blocked inbox ${targetInbox}`);
|
|
115
136
|
return;
|
|
@@ -147,6 +168,7 @@ function createDeliveryService(config) {
|
|
|
147
168
|
return actor.sharedInboxUrl ?? actor.inboxUrl ?? undefined;
|
|
148
169
|
}
|
|
149
170
|
async function deliverToFollowers(activity, senderOxyUserId, senderUsername, options = {}) {
|
|
171
|
+
await authorizeDelivery(activity, senderOxyUserId);
|
|
150
172
|
const actorUris = await config.follows.listAcceptedInboundFollowerActorUris(senderOxyUserId);
|
|
151
173
|
const actors = actorUris.length > 0 ? await config.store.findActorInboxesByUris(actorUris) : [];
|
|
152
174
|
// Group by shared inbox to avoid duplicate deliveries. Follower inboxes
|
|
@@ -241,17 +263,12 @@ function createDeliveryService(config) {
|
|
|
241
263
|
// cached; if the actor is unknown locally we still record the follow and
|
|
242
264
|
// queue the Follow activity, then refresh the actor in the background.
|
|
243
265
|
const cached = await config.store.findActorByUri(remoteActorUri);
|
|
244
|
-
// Always refresh the actor in the background so its inbox/profile stay
|
|
245
|
-
// current (and so a missing actor gets resolved for delivery shortly).
|
|
246
|
-
config.actorRefresh.refreshActorInBackground(remoteActorUri, cached ?? undefined);
|
|
247
266
|
const canonicalUri = cached?.uri ?? remoteActorUri;
|
|
248
267
|
const localActorUri = urls.actor(localUsername);
|
|
249
268
|
// Use the actor _id when known, otherwise a stable hash of the URI so the
|
|
250
269
|
// activity ID is deterministic across retries before the actor is cached.
|
|
251
270
|
const activityIdSuffix = cached?._id ? String(cached._id) : encodeURIComponent(canonicalUri);
|
|
252
271
|
const activityId = `${localActorUri}/follows/${activityIdSuffix}`;
|
|
253
|
-
// Create or update the follow record
|
|
254
|
-
await config.follows.upsertOutboundPending(localOxyUserId, canonicalUri, activityId);
|
|
255
272
|
const activity = {
|
|
256
273
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
257
274
|
id: activityId,
|
|
@@ -259,11 +276,16 @@ function createDeliveryService(config) {
|
|
|
259
276
|
actor: localActorUri,
|
|
260
277
|
object: canonicalUri,
|
|
261
278
|
};
|
|
279
|
+
// Refuse before committing an outbound follow or reporting success.
|
|
280
|
+
await authorizeDelivery(activity, localOxyUserId);
|
|
281
|
+
// A refused follow must not start remote actor discovery either.
|
|
282
|
+
config.actorRefresh.refreshActorInBackground(remoteActorUri, cached ?? undefined);
|
|
283
|
+
await config.follows.upsertOutboundPending(localOxyUserId, canonicalUri, activityId);
|
|
262
284
|
// If we know the inbox, attempt delivery in the background; otherwise queue
|
|
263
285
|
// for the delivery worker, which resolves the inbox once the actor lands.
|
|
264
286
|
const targetInbox = cached?.sharedInboxUrl ?? cached?.inboxUrl;
|
|
265
287
|
if (targetInbox && !isBlockedUrl(targetInbox)) {
|
|
266
|
-
void
|
|
288
|
+
void deliverAuthorizedActivity(activity, targetInbox, localUsername)
|
|
267
289
|
.then((delivered) => {
|
|
268
290
|
if (!delivered)
|
|
269
291
|
return queueDelivery(activity, targetInbox, localOxyUserId);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.discoveryCacheControl = discoveryCacheControl;
|
|
4
|
+
/** Shared discovery config validation; zero forbids downstream response retention. */
|
|
5
|
+
function discoveryCacheControl(value, defaultSeconds) {
|
|
6
|
+
const seconds = value ?? defaultSeconds;
|
|
7
|
+
if (!Number.isInteger(seconds) || seconds < 0 || seconds > 86400) {
|
|
8
|
+
throw new RangeError('httpCacheTtlSeconds must be an integer from 0 to 86400');
|
|
9
|
+
}
|
|
10
|
+
return seconds === 0 ? 'no-store' : `max-age=${seconds}`;
|
|
11
|
+
}
|
package/dist/cjs/node/index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* actor + inbox + follow-graph Express routers.
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.createActorRouter = exports.createWebfingerRouter = exports.ActorResolutionPendingError = exports.createInboundDispatcher = exports.createDeliveryService = exports.ActorResolver = exports.createActorResolver = exports.createIdentityBridge = exports.createTcoLinkExpander = exports.createSignedFetch = void 0;
|
|
21
|
+
exports.createActorRouter = exports.createWebfingerRouter = exports.ActorResolutionPendingError = exports.createInboundDispatcher = exports.DeliveryAuthorizationDeniedError = exports.createDeliveryService = exports.ActorResolver = exports.createActorResolver = exports.createIdentityBridge = exports.createTcoLinkExpander = exports.createSignedFetch = void 0;
|
|
22
22
|
var signedFetch_1 = require("./signedFetch");
|
|
23
23
|
Object.defineProperty(exports, "createSignedFetch", { enumerable: true, get: function () { return signedFetch_1.createSignedFetch; } });
|
|
24
24
|
/**
|
|
@@ -49,6 +49,7 @@ Object.defineProperty(exports, "ActorResolver", { enumerable: true, get: functio
|
|
|
49
49
|
*/
|
|
50
50
|
var delivery_1 = require("./delivery");
|
|
51
51
|
Object.defineProperty(exports, "createDeliveryService", { enumerable: true, get: function () { return delivery_1.createDeliveryService; } });
|
|
52
|
+
Object.defineProperty(exports, "DeliveryAuthorizationDeniedError", { enumerable: true, get: function () { return delivery_1.DeliveryAuthorizationDeniedError; } });
|
|
52
53
|
/**
|
|
53
54
|
* Inbound ActivityPub dispatch — the untrusted-activity validator + switch, the
|
|
54
55
|
* follow-protocol handlers (Follow / Accept / Undo(Follow) / Reject) over the
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.createWebfingerRouter = createWebfingerRouter;
|
|
17
17
|
const express_1 = require("express");
|
|
18
|
+
const httpCacheControl_1 = require("./httpCacheControl");
|
|
18
19
|
const apUri_1 = require("../apUri");
|
|
19
20
|
const urls_1 = require("../urls");
|
|
20
|
-
/**
|
|
21
|
+
/** Default HTTP response max-age. The injected JRD cache has its own policy. */
|
|
21
22
|
const WEBFINGER_CACHE_TTL = 3600;
|
|
22
23
|
/** 24h — host-meta is effectively static. */
|
|
23
24
|
const HOST_META_CACHE_CONTROL = `max-age=${60 * 60 * 24}`;
|
|
@@ -25,6 +26,7 @@ const HOST_META_CACHE_CONTROL = `max-age=${60 * 60 * 24}`;
|
|
|
25
26
|
function createWebfingerRouter(config) {
|
|
26
27
|
const router = (0, express_1.Router)();
|
|
27
28
|
const { domain } = config;
|
|
29
|
+
const httpCacheControl = (0, httpCacheControl_1.discoveryCacheControl)(config.httpCacheTtlSeconds, WEBFINGER_CACHE_TTL);
|
|
28
30
|
const webfingerTemplate = `https://${domain}/.well-known/webfinger?resource={uri}`;
|
|
29
31
|
router.get('/webfinger', async (req, res) => {
|
|
30
32
|
if (!config.federationEnabled) {
|
|
@@ -77,14 +79,14 @@ function createWebfingerRouter(config) {
|
|
|
77
79
|
],
|
|
78
80
|
};
|
|
79
81
|
res.set('Content-Type', 'application/jrd+json; charset=utf-8');
|
|
80
|
-
res.set('Cache-Control',
|
|
82
|
+
res.set('Cache-Control', httpCacheControl);
|
|
81
83
|
return res.json(instanceJrd);
|
|
82
84
|
}
|
|
83
85
|
// Check the JRD cache first.
|
|
84
86
|
const cached = await config.cache.get(username);
|
|
85
87
|
if (cached) {
|
|
86
88
|
res.set('Content-Type', 'application/jrd+json; charset=utf-8');
|
|
87
|
-
res.set('Cache-Control',
|
|
89
|
+
res.set('Cache-Control', httpCacheControl);
|
|
88
90
|
return res.json(cached);
|
|
89
91
|
}
|
|
90
92
|
const user = await config.resolveUser(username);
|
|
@@ -125,7 +127,7 @@ function createWebfingerRouter(config) {
|
|
|
125
127
|
};
|
|
126
128
|
config.cache.set(username, response);
|
|
127
129
|
res.set('Content-Type', 'application/jrd+json; charset=utf-8');
|
|
128
|
-
res.set('Cache-Control',
|
|
130
|
+
res.set('Cache-Control', httpCacheControl);
|
|
129
131
|
return res.json(response);
|
|
130
132
|
}
|
|
131
133
|
catch (err) {
|