@oxy.so/federation 2.2.1 → 2.3.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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/apUri.js +4 -2
- package/dist/cjs/httpSignature.js +1 -1
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/networkIdentity.js +3 -2
- package/dist/cjs/node/actorResolver.js +24 -20
- package/dist/cjs/node/actorRouter.js +12 -4
- package/dist/cjs/node/delivery.js +7 -4
- package/dist/cjs/node/inboundDispatch.js +4 -3
- package/dist/cjs/node/index.js +7 -1
- package/dist/cjs/node/tcoLinks.js +97 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/apUri.js +4 -2
- package/dist/esm/httpSignature.js +1 -1
- package/dist/esm/index.js +3 -3
- package/dist/esm/networkIdentity.js +3 -2
- package/dist/esm/node/actorResolver.js +24 -20
- package/dist/esm/node/actorRouter.js +12 -4
- package/dist/esm/node/delivery.js +7 -4
- package/dist/esm/node/inboundDispatch.js +4 -3
- package/dist/esm/node/index.js +5 -0
- package/dist/esm/node/tcoLinks.js +94 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/httpSignature.d.ts +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/node/delivery.d.ts +2 -3
- package/dist/types/node/index.d.ts +5 -0
- package/dist/types/node/tcoLinks.d.ts +41 -0
- package/package.json +2 -3
- package/src/__tests__/actorCollectionCounts.test.ts +27 -18
- package/src/__tests__/actorObject.test.ts +27 -7
- package/src/__tests__/actorResolver.test.ts +37 -22
- package/src/__tests__/actorResolverNetworkIdentity.test.ts +73 -36
- package/src/__tests__/apUri.test.ts +9 -5
- package/src/__tests__/delivery.test.ts +56 -24
- package/src/__tests__/federationHost.test.ts +13 -3
- package/src/__tests__/httpSignature.test.ts +1 -3
- package/src/__tests__/inboundDispatch.test.ts +80 -30
- package/src/__tests__/networkIdentity.test.ts +240 -113
- package/src/__tests__/routers.test.ts +87 -27
- package/src/__tests__/tcoLinks.test.ts +21 -0
- package/src/actorObject.ts +8 -3
- package/src/apUri.ts +6 -2
- package/src/httpSignature.ts +1 -1
- package/src/index.ts +8 -3
- package/src/networkIdentity.ts +14 -5
- package/src/node/actorResolver.ts +91 -50
- package/src/node/actorRouter.ts +38 -12
- package/src/node/delivery.ts +55 -21
- package/src/node/identityBridge.ts +38 -18
- package/src/node/inboundDispatch.ts +33 -13
- package/src/node/index.ts +11 -0
- package/src/node/signedFetch.ts +3 -1
- package/src/node/tcoLinks.ts +114 -0
|
@@ -50,7 +50,8 @@ function makeWebfingerApp(overrides: Partial<WebfingerRouterConfig> = {}): Expre
|
|
|
50
50
|
resolveUser: async (username) => (username === 'alice' ? { _id: 'u-alice' } : null),
|
|
51
51
|
consent: {
|
|
52
52
|
isSharingEnabledFromUser: () => true,
|
|
53
|
-
getSharingStateByUsername: async (username) =>
|
|
53
|
+
getSharingStateByUsername: async (username) =>
|
|
54
|
+
username === 'alice' ? 'enabled' : 'unknown-user',
|
|
54
55
|
},
|
|
55
56
|
cache: { get: async () => null, set: () => {} },
|
|
56
57
|
logger: { error: () => {} },
|
|
@@ -60,10 +61,12 @@ function makeWebfingerApp(overrides: Partial<WebfingerRouterConfig> = {}): Expre
|
|
|
60
61
|
return app;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function makeActorApp(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
function makeActorApp(
|
|
65
|
+
overrides: {
|
|
66
|
+
actorConfig?: Partial<ActorRouterConfig>;
|
|
67
|
+
onDispatch?: (activity: Record<string, unknown>, verifiedActorUri: string) => void;
|
|
68
|
+
} = {},
|
|
69
|
+
): Express {
|
|
67
70
|
const app = express();
|
|
68
71
|
app.use(
|
|
69
72
|
express.json({
|
|
@@ -80,17 +83,30 @@ function makeActorApp(overrides: {
|
|
|
80
83
|
apContentType: 'application/activity+json',
|
|
81
84
|
urls,
|
|
82
85
|
wantsActivityPub,
|
|
83
|
-
getPublicKey: async () => ({
|
|
84
|
-
|
|
86
|
+
getPublicKey: async () => ({
|
|
87
|
+
keyId: `${urls.actor('alice')}#main-key`,
|
|
88
|
+
publicKeyPem: 'LOCALPEM',
|
|
89
|
+
}),
|
|
90
|
+
resolveUser: async (username) =>
|
|
91
|
+
username === 'alice'
|
|
92
|
+
? { _id: 'u-alice', name: { displayName: 'Alice' }, _count: { followers: 3, following: 1 } }
|
|
93
|
+
: null,
|
|
85
94
|
consent: {
|
|
86
95
|
isSharingEnabledFromUser: () => true,
|
|
87
|
-
getSharingStateByUsername: async (username) =>
|
|
96
|
+
getSharingStateByUsername: async (username) =>
|
|
97
|
+
username === 'alice' ? 'enabled' : 'unknown-user',
|
|
88
98
|
},
|
|
89
|
-
buildLocalActorObject: (params) => ({
|
|
99
|
+
buildLocalActorObject: (params) => ({
|
|
100
|
+
id: urls.actor(params.username),
|
|
101
|
+
type: 'Person',
|
|
102
|
+
name: params.displayName,
|
|
103
|
+
}),
|
|
90
104
|
getBanner: async () => null,
|
|
91
105
|
inbound: {
|
|
92
106
|
fetchPublicKey: async (keyId) =>
|
|
93
|
-
keyId === REMOTE_KEY_ID
|
|
107
|
+
keyId === REMOTE_KEY_ID
|
|
108
|
+
? { publicKeyPem: REMOTE_PUBLIC_PEM, actorUri: REMOTE_ACTOR }
|
|
109
|
+
: null,
|
|
94
110
|
trustForwardedHost: true,
|
|
95
111
|
enqueueInboxActivity: async () => false, // force the inline dispatch path
|
|
96
112
|
processInboxActivity: async (activity, verifiedActorUri) => {
|
|
@@ -108,23 +124,35 @@ function makeActorApp(overrides: {
|
|
|
108
124
|
|
|
109
125
|
describe('webfinger router', () => {
|
|
110
126
|
it('resolves acct:alice@mention.earth to the actor self link (200)', async () => {
|
|
111
|
-
const res = await request(makeWebfingerApp())
|
|
127
|
+
const res = await request(makeWebfingerApp())
|
|
128
|
+
.get('/.well-known/webfinger')
|
|
129
|
+
.query({ resource: 'acct:alice@mention.earth' });
|
|
112
130
|
expect(res.status).toBe(200);
|
|
113
131
|
expect(res.body.subject).toBe('acct:alice@mention.earth');
|
|
114
|
-
expect(res.body.links[0]).toEqual({
|
|
132
|
+
expect(res.body.links[0]).toEqual({
|
|
133
|
+
rel: 'self',
|
|
134
|
+
type: 'application/activity+json',
|
|
135
|
+
href: urls.actor('alice'),
|
|
136
|
+
});
|
|
115
137
|
});
|
|
116
138
|
|
|
117
139
|
it('normalizes mixed-case acct local-parts before resolve', async () => {
|
|
118
|
-
const resolveUser = jest.fn(async (username: string) =>
|
|
140
|
+
const resolveUser = jest.fn(async (username: string) =>
|
|
141
|
+
username === 'alice' ? { _id: 'u-alice' } : null,
|
|
142
|
+
);
|
|
119
143
|
const app = makeWebfingerApp({ resolveUser });
|
|
120
|
-
const res = await request(app)
|
|
144
|
+
const res = await request(app)
|
|
145
|
+
.get('/.well-known/webfinger')
|
|
146
|
+
.query({ resource: 'acct:Alice@mention.earth' });
|
|
121
147
|
expect(res.status).toBe(200);
|
|
122
148
|
expect(resolveUser).toHaveBeenCalledWith('alice');
|
|
123
149
|
expect(res.body.subject).toBe('acct:alice@mention.earth');
|
|
124
150
|
});
|
|
125
151
|
|
|
126
152
|
it('404s an unknown domain', async () => {
|
|
127
|
-
const res = await request(makeWebfingerApp())
|
|
153
|
+
const res = await request(makeWebfingerApp())
|
|
154
|
+
.get('/.well-known/webfinger')
|
|
155
|
+
.query({ resource: 'acct:alice@other.example' });
|
|
128
156
|
expect(res.status).toBe(404);
|
|
129
157
|
});
|
|
130
158
|
|
|
@@ -138,9 +166,14 @@ describe('webfinger router', () => {
|
|
|
138
166
|
|
|
139
167
|
it('404s a sharing-disabled user indistinguishably', async () => {
|
|
140
168
|
const app = makeWebfingerApp({
|
|
141
|
-
consent: {
|
|
169
|
+
consent: {
|
|
170
|
+
isSharingEnabledFromUser: () => false,
|
|
171
|
+
getSharingStateByUsername: async () => 'disabled',
|
|
172
|
+
},
|
|
142
173
|
});
|
|
143
|
-
const res = await request(app)
|
|
174
|
+
const res = await request(app)
|
|
175
|
+
.get('/.well-known/webfinger')
|
|
176
|
+
.query({ resource: 'acct:alice@mention.earth' });
|
|
144
177
|
expect(res.status).toBe(404);
|
|
145
178
|
expect(res.body).toEqual({ error: 'User not found' });
|
|
146
179
|
});
|
|
@@ -193,9 +226,7 @@ describe('webfinger router — the instance (server) actor', () => {
|
|
|
193
226
|
expect(webfinger.status).toBe(200);
|
|
194
227
|
expect(actor.status).toBe(200);
|
|
195
228
|
|
|
196
|
-
const selfLink = webfinger.body.links.find(
|
|
197
|
-
(link: { rel: string }) => link.rel === 'self',
|
|
198
|
-
);
|
|
229
|
+
const selfLink = webfinger.body.links.find((link: { rel: string }) => link.rel === 'self');
|
|
199
230
|
expect(selfLink).toBeDefined();
|
|
200
231
|
// Positive control: neither side may be vacuously undefined.
|
|
201
232
|
expect(typeof actor.body.id).toBe('string');
|
|
@@ -333,17 +364,34 @@ describe('actor router — GET /ap/users/:username', () => {
|
|
|
333
364
|
|
|
334
365
|
describe('actor router — inbox POST', () => {
|
|
335
366
|
/** Build a validly-signed inbox request body + headers for `activity`. */
|
|
336
|
-
async function signedInbox(
|
|
367
|
+
async function signedInbox(
|
|
368
|
+
path: string,
|
|
369
|
+
activity: Record<string, unknown>,
|
|
370
|
+
): Promise<{ body: string; headers: Record<string, string> }> {
|
|
337
371
|
const body = JSON.stringify(activity);
|
|
338
|
-
const headers = await signRequest(
|
|
372
|
+
const headers = await signRequest(
|
|
373
|
+
sign,
|
|
374
|
+
REMOTE_KEY_ID,
|
|
375
|
+
'POST',
|
|
376
|
+
`https://${DOMAIN}${path}`,
|
|
377
|
+
body,
|
|
378
|
+
);
|
|
339
379
|
return { body, headers };
|
|
340
380
|
}
|
|
341
381
|
|
|
342
382
|
it('accepts a validly-signed activity (202) and reaches the dispatcher — no redirect', async () => {
|
|
343
383
|
const dispatched: Array<{ activity: Record<string, unknown>; verifiedActorUri: string }> = [];
|
|
344
|
-
const app = makeActorApp({
|
|
384
|
+
const app = makeActorApp({
|
|
385
|
+
onDispatch: (activity, verifiedActorUri) => dispatched.push({ activity, verifiedActorUri }),
|
|
386
|
+
});
|
|
345
387
|
|
|
346
|
-
const activity = {
|
|
388
|
+
const activity = {
|
|
389
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
390
|
+
id: 'https://remote.example/f/1',
|
|
391
|
+
type: 'Follow',
|
|
392
|
+
actor: REMOTE_ACTOR,
|
|
393
|
+
object: urls.actor('alice'),
|
|
394
|
+
};
|
|
347
395
|
const { body, headers } = await signedInbox('/ap/inbox', activity);
|
|
348
396
|
|
|
349
397
|
const res = await request(app)
|
|
@@ -399,7 +447,9 @@ describe('actor router — inbox POST', () => {
|
|
|
399
447
|
actorConfig: {
|
|
400
448
|
inbound: {
|
|
401
449
|
fetchPublicKey: async (keyId) =>
|
|
402
|
-
keyId === REMOTE_KEY_ID
|
|
450
|
+
keyId === REMOTE_KEY_ID
|
|
451
|
+
? { publicKeyPem: REMOTE_PUBLIC_PEM, actorUri: REMOTE_ACTOR }
|
|
452
|
+
: null,
|
|
403
453
|
trustForwardedHost: true,
|
|
404
454
|
enqueueInboxActivity: async () => false,
|
|
405
455
|
processInboxActivity: dispatcher.processInboxActivity,
|
|
@@ -436,7 +486,12 @@ describe('actor router — inbox POST', () => {
|
|
|
436
486
|
});
|
|
437
487
|
|
|
438
488
|
it('rejects an actor mismatch with 403', async () => {
|
|
439
|
-
const activity = {
|
|
489
|
+
const activity = {
|
|
490
|
+
id: 'https://remote.example/f/2',
|
|
491
|
+
type: 'Follow',
|
|
492
|
+
actor: 'https://evil.example/users/x',
|
|
493
|
+
object: urls.actor('alice'),
|
|
494
|
+
};
|
|
440
495
|
const { body, headers } = await signedInbox('/ap/inbox', activity);
|
|
441
496
|
const res = await request(makeActorApp())
|
|
442
497
|
.post('/ap/inbox')
|
|
@@ -448,7 +503,12 @@ describe('actor router — inbox POST', () => {
|
|
|
448
503
|
});
|
|
449
504
|
|
|
450
505
|
it('404s a user inbox for a sharing-disabled user (before signature verify)', async () => {
|
|
451
|
-
const activity = {
|
|
506
|
+
const activity = {
|
|
507
|
+
id: 'https://remote.example/f/3',
|
|
508
|
+
type: 'Follow',
|
|
509
|
+
actor: REMOTE_ACTOR,
|
|
510
|
+
object: urls.actor('ghost'),
|
|
511
|
+
};
|
|
452
512
|
const { body, headers } = await signedInbox('/ap/users/ghost/inbox', activity);
|
|
453
513
|
const res = await request(makeActorApp())
|
|
454
514
|
.post('/ap/users/ghost/inbox')
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createTcoLinkExpander } from '../node/tcoLinks';
|
|
2
|
+
import type { SingleHopFetch } from '../node/signedFetch';
|
|
3
|
+
|
|
4
|
+
const fetchSingleHop: SingleHopFetch = async (url) => {
|
|
5
|
+
const location = { 'https://t.co/abc123': 'https://example.com/', 'https://t.co/self12': url }[
|
|
6
|
+
url
|
|
7
|
+
];
|
|
8
|
+
return location
|
|
9
|
+
? new Response(null, { status: 301, headers: { location } })
|
|
10
|
+
: new Response(null, { status: 404 });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it('replaces a t.co wrapper with its target and leaves anything else as written', async () => {
|
|
14
|
+
const expand = createTcoLinkExpander({ fetchSingleHop, userAgent: 'test' });
|
|
15
|
+
|
|
16
|
+
await expect(
|
|
17
|
+
expand('a https://t.co/abc123 b https://t.co/gone12 c https://t.co/self12 d https://bit.ly/x'),
|
|
18
|
+
).resolves.toBe(
|
|
19
|
+
'a https://example.com/ b https://t.co/gone12 c https://t.co/self12 d https://bit.ly/x',
|
|
20
|
+
);
|
|
21
|
+
});
|
package/src/actorObject.ts
CHANGED
|
@@ -257,7 +257,9 @@ function buildActorIcon(
|
|
|
257
257
|
if (!avatar) return undefined;
|
|
258
258
|
const resolved = config.media.resolveAvatar(avatar);
|
|
259
259
|
if (!resolved || !isAbsoluteHttpUrl(resolved)) {
|
|
260
|
-
config.onWarn?.(
|
|
260
|
+
config.onWarn?.(
|
|
261
|
+
`[Federation] Omitting actor icon — avatar did not resolve to an absolute URL (ref: ${avatar})`,
|
|
262
|
+
);
|
|
261
263
|
return undefined;
|
|
262
264
|
}
|
|
263
265
|
return apImageObject(resolved);
|
|
@@ -275,7 +277,9 @@ function buildActorImage(
|
|
|
275
277
|
if (!banner) return undefined;
|
|
276
278
|
const resolved = config.media.resolveBanner(banner);
|
|
277
279
|
if (!resolved || !isAbsoluteHttpUrl(resolved)) {
|
|
278
|
-
config.onWarn?.(
|
|
280
|
+
config.onWarn?.(
|
|
281
|
+
`[Federation] Omitting actor image — banner did not resolve to an absolute URL (ref: ${banner})`,
|
|
282
|
+
);
|
|
279
283
|
return undefined;
|
|
280
284
|
}
|
|
281
285
|
return apImageObject(resolved);
|
|
@@ -309,7 +313,8 @@ export function normalizeAlsoKnownAs(values: readonly string[] | null | undefine
|
|
|
309
313
|
*/
|
|
310
314
|
export function createLocalActorBuilder(config: LocalActorBuilderConfig): LocalActorBuilder {
|
|
311
315
|
return (params: BuildLocalActorParams): Record<string, unknown> => {
|
|
312
|
-
const { username, displayName, kind, bio, avatar, profileHeaderImage, publicKey, createdAt } =
|
|
316
|
+
const { username, displayName, kind, bio, avatar, profileHeaderImage, publicKey, createdAt } =
|
|
317
|
+
params;
|
|
313
318
|
|
|
314
319
|
const actorObject: Record<string, unknown> = {
|
|
315
320
|
id: config.urls.actor(username),
|
package/src/apUri.ts
CHANGED
|
@@ -135,7 +135,9 @@ export function createDomainPolicy(config: DomainPolicyConfig): DomainPolicy {
|
|
|
135
135
|
canonicalFederationHost(config.domain),
|
|
136
136
|
canonicalFederationHost(config.actorDomain ?? config.domain),
|
|
137
137
|
]);
|
|
138
|
-
const identityApex = config.identityApex
|
|
138
|
+
const identityApex = config.identityApex
|
|
139
|
+
? canonicalFederationHost(config.identityApex)
|
|
140
|
+
: undefined;
|
|
139
141
|
const blocked = new Set<string>();
|
|
140
142
|
for (const d of config.blockedDomains ?? []) {
|
|
141
143
|
blocked.add(canonicalFederationHost(d));
|
|
@@ -144,7 +146,9 @@ export function createDomainPolicy(config: DomainPolicyConfig): DomainPolicy {
|
|
|
144
146
|
return {
|
|
145
147
|
isBlockedDomain(domain: string): boolean {
|
|
146
148
|
const d = canonicalFederationHost(domain);
|
|
147
|
-
return
|
|
149
|
+
return (
|
|
150
|
+
localDomains.has(d) || (identityApex !== undefined && d === identityApex) || blocked.has(d)
|
|
151
|
+
);
|
|
148
152
|
},
|
|
149
153
|
extractLocalPostId(objectUri: string): string | null {
|
|
150
154
|
let parsed: URL;
|
package/src/httpSignature.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Mention runs behind a CF-proxied apex that rewrites the origin `Host`, so it
|
|
17
17
|
* passes `true`; a directly-exposed origin leaves it `false`.
|
|
18
18
|
*
|
|
19
|
-
* Lives in the isomorphic `.` entry (no Express /
|
|
19
|
+
* Lives in the isomorphic `.` entry (no Express / database driver): it depends only on
|
|
20
20
|
* the runtime `crypto` builtin (Node / Bun) and is never invoked from browser /
|
|
21
21
|
* React-Native bundles — RN consumers import only the connector TYPES, which are
|
|
22
22
|
* erased at compile time.
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* connector produces, the local-event union connectors deliver outbound, and
|
|
9
9
|
* the connector interface itself.
|
|
10
10
|
*
|
|
11
|
-
* IMPORTANT: this entry is intentionally free of
|
|
12
|
-
* Native so it can be imported from any Oxy app backend (and, in later phases,
|
|
11
|
+
* IMPORTANT: this entry is intentionally free of database drivers / Express /
|
|
12
|
+
* React Native so it can be imported from any Oxy app backend (and, in later phases,
|
|
13
13
|
* share the pure HTTP-signature + actor-object surface with browser/isomorphic
|
|
14
14
|
* callers). The runnable Express/Node engine — signed fetch, delivery transport,
|
|
15
15
|
* webfinger/actor/inbox routers, remote-actor resolution — lives under the
|
|
@@ -43,7 +43,12 @@ export {
|
|
|
43
43
|
* Domain-parameterized ActivityPub URL builders — each app instantiates them once
|
|
44
44
|
* with its own `FEDERATION_DOMAIN` so every actor stays `@user@its-own-domain`.
|
|
45
45
|
*/
|
|
46
|
-
export {
|
|
46
|
+
export {
|
|
47
|
+
createUrlBuilders,
|
|
48
|
+
normalizeActorUsername,
|
|
49
|
+
INSTANCE_ACTOR_USERNAME,
|
|
50
|
+
type UrlBuilders,
|
|
51
|
+
} from './urls';
|
|
47
52
|
|
|
48
53
|
/**
|
|
49
54
|
* Network identity: the MECHANISM for re-labelling an account republished by a
|
package/src/networkIdentity.ts
CHANGED
|
@@ -168,7 +168,8 @@ export function parseUpstreamProfileUrl(
|
|
|
168
168
|
|
|
169
169
|
const host = canonicalFederationHost(url.hostname);
|
|
170
170
|
for (const network of networks) {
|
|
171
|
-
if (!network.profileHosts.some((allowed) => canonicalFederationHost(allowed) === host))
|
|
171
|
+
if (!network.profileHosts.some((allowed) => canonicalFederationHost(allowed) === host))
|
|
172
|
+
continue;
|
|
172
173
|
const handle = profileUrlHandle(url.href, network.profileHosts, network.profilePathPrefix);
|
|
173
174
|
if (handle !== undefined && handle.length > 0) return { network, handle };
|
|
174
175
|
}
|
|
@@ -457,10 +458,16 @@ export function upstreamHandleFromProfileField(options: {
|
|
|
457
458
|
// Observed bird.makeup Official assertion, 2026-09-13:
|
|
458
459
|
// https://https://twitter.com/jordievole. Repair exactly one duplicated
|
|
459
460
|
// HTTPS prefix, then run the ordinary host/path/credential validation.
|
|
460
|
-
const candidateHref =
|
|
461
|
-
|
|
461
|
+
const candidateHref =
|
|
462
|
+
options.repairRepeatedHttpsScheme && href.startsWith('https://https://')
|
|
463
|
+
? href.slice('https://'.length)
|
|
464
|
+
: href;
|
|
462
465
|
let handle: string | undefined;
|
|
463
|
-
try {
|
|
466
|
+
try {
|
|
467
|
+
handle = profileUrlHandle(candidateHref, options.hosts, prefix);
|
|
468
|
+
} catch {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
464
471
|
if (handle !== undefined && handle.length > 0) handles.set(handle.toLowerCase(), handle);
|
|
465
472
|
}
|
|
466
473
|
}
|
|
@@ -681,7 +688,9 @@ export function createBridgeRelabeller(
|
|
|
681
688
|
vouchesForNetwork: (actorHost, networkDomain) => {
|
|
682
689
|
const bridge = findBridge(actorHost);
|
|
683
690
|
if (!bridge) return false;
|
|
684
|
-
return
|
|
691
|
+
return (
|
|
692
|
+
canonicalFederationHost(bridge.network.domain) === canonicalFederationHost(networkDomain)
|
|
693
|
+
);
|
|
685
694
|
},
|
|
686
695
|
deriveNetworkIdentity: (candidate) => {
|
|
687
696
|
const entry = findBridge(candidate.host);
|