@oxy.so/federation 2.2.0 → 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 +3 -4
- 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
|
@@ -32,7 +32,10 @@ function entry(overrides: Partial<FederationBridgeEntry> = {}): FederationBridge
|
|
|
32
32
|
network: FEDERATION_NETWORKS.x,
|
|
33
33
|
operator: 'Test operator',
|
|
34
34
|
software: 'TestBridge',
|
|
35
|
-
derive: upstreamHandleFromProfileField({
|
|
35
|
+
derive: upstreamHandleFromProfileField({
|
|
36
|
+
fieldName: 'Official',
|
|
37
|
+
hosts: ['twitter.com', 'x.com'],
|
|
38
|
+
}),
|
|
36
39
|
caseRule: 'lowercase',
|
|
37
40
|
relabel: 'enabled',
|
|
38
41
|
upstreamIdStability: 'recyclable',
|
|
@@ -62,17 +65,45 @@ function candidate(overrides: Partial<NetworkIdentityCandidate> = {}): NetworkId
|
|
|
62
65
|
|
|
63
66
|
describe('createBridgeRelabeller', () => {
|
|
64
67
|
it('refuses contradictory accepted profile assertions regardless of their order', () => {
|
|
65
|
-
const derive = upstreamHandleFromProfileField({
|
|
66
|
-
|
|
68
|
+
const derive = upstreamHandleFromProfileField({
|
|
69
|
+
fieldName: 'Official',
|
|
70
|
+
hosts: ['x.com'],
|
|
71
|
+
requireRelMe: true,
|
|
72
|
+
});
|
|
73
|
+
const fields = ['alice', 'bob'].map((handle) => ({
|
|
74
|
+
name: 'Official',
|
|
75
|
+
value: `<a href="https://x.com/${handle}" rel="me">Official</a>`,
|
|
76
|
+
}));
|
|
67
77
|
expect(derive(candidate({ fields }))).toBeUndefined();
|
|
68
78
|
expect(derive(candidate({ fields: [...fields].reverse() }))).toBeUndefined();
|
|
69
79
|
expect(derive(candidate({ fields: [fields[0], fields[0]] }))).toBe('alice');
|
|
70
|
-
expect(
|
|
80
|
+
expect(
|
|
81
|
+
derive(
|
|
82
|
+
candidate({
|
|
83
|
+
fields: [{ name: 'Official', value: '<a href="https://x.com/alice">Official</a>' }],
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
).toBeUndefined();
|
|
71
87
|
});
|
|
72
88
|
it('repairs the observed double-HTTPS Official assertion only with the reviewed opt-in', () => {
|
|
73
|
-
const actor = candidate({
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
const actor = candidate({
|
|
90
|
+
fields: [
|
|
91
|
+
{
|
|
92
|
+
name: 'Official',
|
|
93
|
+
value: '<a href="https://https://twitter.com/jordievole" rel="me">Official</a>',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
expect(
|
|
98
|
+
upstreamHandleFromProfileField({ fieldName: 'Official', hosts: ['twitter.com'] })(actor),
|
|
99
|
+
).toBeUndefined();
|
|
100
|
+
expect(
|
|
101
|
+
upstreamHandleFromProfileField({
|
|
102
|
+
fieldName: 'Official',
|
|
103
|
+
hosts: ['twitter.com'],
|
|
104
|
+
repairRepeatedHttpsScheme: true,
|
|
105
|
+
})(actor),
|
|
106
|
+
).toBe('jordievole');
|
|
76
107
|
});
|
|
77
108
|
|
|
78
109
|
it.each([
|
|
@@ -83,9 +114,17 @@ describe('createBridgeRelabeller', () => {
|
|
|
83
114
|
'https://https://https://twitter.com/jordievole',
|
|
84
115
|
'http://https://twitter.com/jordievole',
|
|
85
116
|
'https://https://twitter.com/i/status/123',
|
|
86
|
-
])('does not widen the repair into an attribution for %s', href => {
|
|
87
|
-
const actor = candidate({
|
|
88
|
-
|
|
117
|
+
])('does not widen the repair into an attribution for %s', (href) => {
|
|
118
|
+
const actor = candidate({
|
|
119
|
+
fields: [{ name: 'Official', value: `<a href="${href}" rel="me">Official</a>` }],
|
|
120
|
+
});
|
|
121
|
+
expect(
|
|
122
|
+
upstreamHandleFromProfileField({
|
|
123
|
+
fieldName: 'Official',
|
|
124
|
+
hosts: ['twitter.com'],
|
|
125
|
+
repairRepeatedHttpsScheme: true,
|
|
126
|
+
})(actor),
|
|
127
|
+
).toBeUndefined();
|
|
89
128
|
});
|
|
90
129
|
it('re-labels an actor onto the network its bridge mirrors', () => {
|
|
91
130
|
const identity = createBridgeRelabeller([entry()]).deriveNetworkIdentity(candidate());
|
|
@@ -96,7 +135,9 @@ describe('createBridgeRelabeller', () => {
|
|
|
96
135
|
|
|
97
136
|
it('declines an actor from a host no entry names', () => {
|
|
98
137
|
const relabeller = createBridgeRelabeller([entry()]);
|
|
99
|
-
expect(
|
|
138
|
+
expect(
|
|
139
|
+
relabeller.deriveNetworkIdentity(candidate({ host: 'mastodon.social' })),
|
|
140
|
+
).toBeUndefined();
|
|
100
141
|
});
|
|
101
142
|
|
|
102
143
|
it('ships no entries of its own — an empty registry re-labels nothing', () => {
|
|
@@ -104,8 +145,9 @@ describe('createBridgeRelabeller', () => {
|
|
|
104
145
|
});
|
|
105
146
|
|
|
106
147
|
it('preserves case where the entry says the handle is already canonical', () => {
|
|
107
|
-
const identity = createBridgeRelabeller([
|
|
108
|
-
|
|
148
|
+
const identity = createBridgeRelabeller([
|
|
149
|
+
entry({ caseRule: 'preserve' }),
|
|
150
|
+
]).deriveNetworkIdentity(candidate());
|
|
109
151
|
expect(identity?.federatedUsername).toBe('WIRED@x.com');
|
|
110
152
|
});
|
|
111
153
|
});
|
|
@@ -138,56 +180,90 @@ describe('createBridgeRelabeller — derivations it refuses', () => {
|
|
|
138
180
|
it.each([
|
|
139
181
|
['an empty string', ''],
|
|
140
182
|
['whitespace only', ' '],
|
|
141
|
-
])(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
183
|
+
])(
|
|
184
|
+
'refuses a rule that yields %s, rather than collapsing a domain onto one identity',
|
|
185
|
+
(_label, derived) => {
|
|
186
|
+
// Asserted against the relabeller's OWN guard, with a rule that returns the
|
|
187
|
+
// bad value directly. Driving it through `upstreamHandleFromPreferredUsername`
|
|
188
|
+
// would prove nothing: that helper filters empties itself, so the outer guard
|
|
189
|
+
// is never reached and the assertion passes with the guard deleted.
|
|
190
|
+
const relabeller = createBridgeRelabeller([entry({ derive: () => derived })]);
|
|
191
|
+
expect(relabeller.deriveNetworkIdentity(candidate())).toBeUndefined();
|
|
192
|
+
},
|
|
193
|
+
);
|
|
149
194
|
|
|
150
195
|
it('refuses an empty preferredUsername, which we hold real actors with', () => {
|
|
151
196
|
const relabeller = createBridgeRelabeller([
|
|
152
197
|
entry({ derive: upstreamHandleFromPreferredUsername([/./]) }),
|
|
153
198
|
]);
|
|
154
199
|
expect(relabeller.deriveNetworkIdentity(candidate({ preferredUsername: '' }))).toBeUndefined();
|
|
155
|
-
expect(
|
|
200
|
+
expect(
|
|
201
|
+
relabeller.deriveNetworkIdentity(candidate({ preferredUsername: ' ' })),
|
|
202
|
+
).toBeUndefined();
|
|
156
203
|
});
|
|
157
204
|
|
|
158
205
|
it('refuses a handle carrying an at-sign or a slash', () => {
|
|
159
206
|
const relabeller = createBridgeRelabeller([
|
|
160
207
|
entry({ derive: upstreamHandleFromPreferredUsername([/./]) }),
|
|
161
208
|
]);
|
|
162
|
-
expect(
|
|
163
|
-
|
|
209
|
+
expect(
|
|
210
|
+
relabeller.deriveNetworkIdentity(candidate({ preferredUsername: 'a@b' })),
|
|
211
|
+
).toBeUndefined();
|
|
212
|
+
expect(
|
|
213
|
+
relabeller.deriveNetworkIdentity(candidate({ preferredUsername: 'a/b' })),
|
|
214
|
+
).toBeUndefined();
|
|
164
215
|
});
|
|
165
216
|
|
|
166
217
|
it('derives nothing when the backlink points off the declared network', () => {
|
|
167
218
|
const relabeller = createBridgeRelabeller([entry()]);
|
|
168
|
-
expect(
|
|
169
|
-
|
|
170
|
-
|
|
219
|
+
expect(
|
|
220
|
+
relabeller.deriveNetworkIdentity(
|
|
221
|
+
candidate({
|
|
222
|
+
fields: [{ name: 'Official', value: '<a href="https://example.com/wired">x</a>' }],
|
|
223
|
+
}),
|
|
224
|
+
),
|
|
225
|
+
).toBeUndefined();
|
|
171
226
|
});
|
|
172
227
|
|
|
173
228
|
it('derives nothing from a link that is not a bare profile path', () => {
|
|
174
229
|
const relabeller = createBridgeRelabeller([entry()]);
|
|
175
|
-
expect(
|
|
176
|
-
|
|
177
|
-
|
|
230
|
+
expect(
|
|
231
|
+
relabeller.deriveNetworkIdentity(
|
|
232
|
+
candidate({
|
|
233
|
+
fields: [{ name: 'Official', value: '<a href="https://twitter.com/i/status/1">x</a>' }],
|
|
234
|
+
}),
|
|
235
|
+
),
|
|
236
|
+
).toBeUndefined();
|
|
178
237
|
});
|
|
179
238
|
|
|
180
239
|
it('matches profile hosts canonically, so a www. prefix on either side still round-trips', () => {
|
|
181
240
|
const relabeller = createBridgeRelabeller([
|
|
182
|
-
entry({
|
|
241
|
+
entry({
|
|
242
|
+
derive: upstreamHandleFromProfileField({
|
|
243
|
+
fieldName: 'Official',
|
|
244
|
+
hosts: ['www.twitter.com'],
|
|
245
|
+
}),
|
|
246
|
+
}),
|
|
183
247
|
]);
|
|
184
|
-
expect(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
248
|
+
expect(
|
|
249
|
+
relabeller.deriveNetworkIdentity(
|
|
250
|
+
candidate({
|
|
251
|
+
fields: [
|
|
252
|
+
{ name: 'Official', value: '<a href="https://twitter.com/WIRED" rel="me">x</a>' },
|
|
253
|
+
],
|
|
254
|
+
proxyOf: [],
|
|
255
|
+
}),
|
|
256
|
+
)?.federatedUsername,
|
|
257
|
+
).toBe('wired@x.com');
|
|
258
|
+
expect(
|
|
259
|
+
relabeller.deriveNetworkIdentity(
|
|
260
|
+
candidate({
|
|
261
|
+
fields: [
|
|
262
|
+
{ name: 'Official', value: '<a href="https://www.twitter.com/WIRED" rel="me">x</a>' },
|
|
263
|
+
],
|
|
264
|
+
}),
|
|
265
|
+
)?.federatedUsername,
|
|
266
|
+
).toBe('wired@x.com');
|
|
191
267
|
});
|
|
192
268
|
|
|
193
269
|
it('derives a Bluesky handle from alsoKnownAs profile URLs (Bridgy Fed pattern)', () => {
|
|
@@ -204,17 +280,18 @@ describe('createBridgeRelabeller — derivations it refuses', () => {
|
|
|
204
280
|
},
|
|
205
281
|
}),
|
|
206
282
|
]);
|
|
207
|
-
expect(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
283
|
+
expect(
|
|
284
|
+
relabeller.deriveNetworkIdentity(
|
|
285
|
+
candidate({
|
|
286
|
+
host: 'bsky.brid.gy',
|
|
287
|
+
acct: 'jay.bsky.team@bsky.brid.gy',
|
|
288
|
+
preferredUsername: 'jay.bsky.team',
|
|
289
|
+
alsoKnownAs: ['at://did:plc:abc123', 'https://bsky.app/profile/jay.bsky.team'],
|
|
290
|
+
fields: [],
|
|
291
|
+
bio: '',
|
|
292
|
+
}),
|
|
293
|
+
)?.federatedUsername,
|
|
294
|
+
).toBe('jay.bsky.team@bsky.social');
|
|
218
295
|
});
|
|
219
296
|
|
|
220
297
|
it('requires the marker before trusting a naming convention', () => {
|
|
@@ -223,9 +300,12 @@ describe('createBridgeRelabeller — derivations it refuses', () => {
|
|
|
223
300
|
const relabeller = createBridgeRelabeller([
|
|
224
301
|
entry({ derive: upstreamHandleFromPreferredUsername([/is a mirror bot\.$/]) }),
|
|
225
302
|
]);
|
|
226
|
-
expect(
|
|
227
|
-
|
|
228
|
-
|
|
303
|
+
expect(
|
|
304
|
+
relabeller.deriveNetworkIdentity(candidate({ bio: 'I run this server.' })),
|
|
305
|
+
).toBeUndefined();
|
|
306
|
+
expect(
|
|
307
|
+
relabeller.deriveNetworkIdentity(candidate({ bio: 'is a mirror bot.' }))?.federatedUsername,
|
|
308
|
+
).toBe('wired@x.com');
|
|
229
309
|
});
|
|
230
310
|
});
|
|
231
311
|
|
|
@@ -303,7 +383,7 @@ describe('upstream profile URLs — one declaration, both directions', () => {
|
|
|
303
383
|
});
|
|
304
384
|
|
|
305
385
|
describe('bsky.social is one network, whichever protocol an account arrives by', () => {
|
|
306
|
-
it(
|
|
386
|
+
it("strips a default handle's redundant suffix and keeps a custom domain whole", () => {
|
|
307
387
|
expect(blueskyUsernameFromHandle('skylee1.bsky.social')).toBe('skylee1');
|
|
308
388
|
expect(blueskyUsernameFromHandle('gothamist.com')).toBe('gothamist.com');
|
|
309
389
|
expect(blueskyUsernameFromHandle('mayor.nyc.gov')).toBe('mayor.nyc.gov');
|
|
@@ -323,8 +403,9 @@ describe('federatedUsernameFromUpstreamUrl — the search direction', () => {
|
|
|
323
403
|
expect(federatedUsernameFromUpstreamUrl('https://x.com/nasa')).toBe('nasa@x.com');
|
|
324
404
|
expect(federatedUsernameFromUpstreamUrl('https://twitter.com/nasa')).toBe('nasa@x.com');
|
|
325
405
|
expect(federatedUsernameFromUpstreamUrl('https://mobile.x.com/nasa')).toBe('nasa@x.com');
|
|
326
|
-
expect(federatedUsernameFromUpstreamUrl('https://www.instagram.com/natgeo'))
|
|
327
|
-
|
|
406
|
+
expect(federatedUsernameFromUpstreamUrl('https://www.instagram.com/natgeo')).toBe(
|
|
407
|
+
'natgeo@instagram.com',
|
|
408
|
+
);
|
|
328
409
|
});
|
|
329
410
|
|
|
330
411
|
it('lowercases, because X and Instagram handles are case-insensitive', () => {
|
|
@@ -332,22 +413,26 @@ describe('federatedUsernameFromUpstreamUrl — the search direction', () => {
|
|
|
332
413
|
expect(federatedUsernameFromUpstreamUrl('https://x.com/WIRED')).toBe('wired@x.com');
|
|
333
414
|
});
|
|
334
415
|
|
|
335
|
-
it(
|
|
416
|
+
it("drops a default Bluesky handle's redundant suffix, exactly as ingest does", () => {
|
|
336
417
|
// The case a second, parallel parsing rule would get wrong: it works for X
|
|
337
418
|
// with plain lowercasing and silently fails here.
|
|
338
|
-
expect(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
419
|
+
expect(
|
|
420
|
+
federatedUsernameFromUpstreamUrl('https://bsky.app/profile/georgemonbiot.bsky.social'),
|
|
421
|
+
).toBe('georgemonbiot@bsky.social');
|
|
422
|
+
expect(federatedUsernameFromUpstreamUrl('https://bsky.app/profile/gothamist.com')).toBe(
|
|
423
|
+
'gothamist.com@bsky.social',
|
|
424
|
+
);
|
|
342
425
|
});
|
|
343
426
|
|
|
344
427
|
it('agrees with what the relabeller would store for the same account', () => {
|
|
345
428
|
// Ingest and search reading one declaration, asserted rather than assumed.
|
|
346
|
-
const relabeller = createBridgeRelabeller([
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
429
|
+
const relabeller = createBridgeRelabeller([
|
|
430
|
+
entry({
|
|
431
|
+
host: 'mirror.example',
|
|
432
|
+
network: FEDERATION_NETWORKS.x,
|
|
433
|
+
derive: () => 'NASA',
|
|
434
|
+
}),
|
|
435
|
+
]);
|
|
351
436
|
const viaIngest = relabeller.deriveNetworkIdentity(candidate())?.federatedUsername;
|
|
352
437
|
expect(federatedUsernameFromUpstreamUrl('https://x.com/NASA')).toBe(viaIngest);
|
|
353
438
|
});
|
|
@@ -366,19 +451,23 @@ describe('FEP-fffd proxyOf', () => {
|
|
|
366
451
|
* the only thing in our corpus that publishes `proxyOf` at all, which is also
|
|
367
452
|
* why no shipped entry names this strategy.
|
|
368
453
|
*/
|
|
369
|
-
const REAL_MOMOSTR_PROXY_OF = [
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
454
|
+
const REAL_MOMOSTR_PROXY_OF = [
|
|
455
|
+
{
|
|
456
|
+
protocol: 'https://github.com/nostr-protocol/nostr',
|
|
457
|
+
proxied: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
|
|
458
|
+
authoritative: true,
|
|
459
|
+
},
|
|
460
|
+
];
|
|
374
461
|
|
|
375
462
|
describe('readProxyDeclarations', () => {
|
|
376
463
|
it('parses a real declaration off the wire', () => {
|
|
377
|
-
expect(readProxyDeclarations(REAL_MOMOSTR_PROXY_OF)).toEqual([
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
464
|
+
expect(readProxyDeclarations(REAL_MOMOSTR_PROXY_OF)).toEqual([
|
|
465
|
+
{
|
|
466
|
+
protocol: 'https://github.com/nostr-protocol/nostr',
|
|
467
|
+
proxied: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
|
|
468
|
+
authoritative: true,
|
|
469
|
+
},
|
|
470
|
+
]);
|
|
382
471
|
});
|
|
383
472
|
|
|
384
473
|
it('defaults authoritative to false when the actor omits it', () => {
|
|
@@ -396,20 +485,24 @@ describe('FEP-fffd proxyOf', () => {
|
|
|
396
485
|
// the flag mean what it says — and a strict check with no test for it is
|
|
397
486
|
// one refactor away from silently becoming the loose one.
|
|
398
487
|
for (const value of ['false', 'true', 1, {}, []]) {
|
|
399
|
-
const [parsed] = readProxyDeclarations([
|
|
488
|
+
const [parsed] = readProxyDeclarations([
|
|
489
|
+
{ protocol: 'p', proxied: 'q', authoritative: value },
|
|
490
|
+
]);
|
|
400
491
|
expect(parsed.authoritative).toBe(false);
|
|
401
492
|
}
|
|
402
493
|
});
|
|
403
494
|
|
|
404
495
|
it('drops malformed entries rather than inventing fields', () => {
|
|
405
|
-
expect(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
496
|
+
expect(
|
|
497
|
+
readProxyDeclarations([
|
|
498
|
+
{ protocol: '', proxied: 'q' },
|
|
499
|
+
{ protocol: 'p', proxied: '' },
|
|
500
|
+
{ protocol: 'p' },
|
|
501
|
+
'not an object',
|
|
502
|
+
null,
|
|
503
|
+
[],
|
|
504
|
+
]),
|
|
505
|
+
).toEqual([]);
|
|
413
506
|
});
|
|
414
507
|
|
|
415
508
|
it('reads a missing or non-array proxyOf as none', () => {
|
|
@@ -431,15 +524,25 @@ describe('FEP-fffd proxyOf', () => {
|
|
|
431
524
|
|
|
432
525
|
it('refuses a non-authoritative declaration', () => {
|
|
433
526
|
// A copy does not get to stand in for the account it copied.
|
|
434
|
-
expect(
|
|
435
|
-
|
|
436
|
-
|
|
527
|
+
expect(
|
|
528
|
+
derive(
|
|
529
|
+
candidate({
|
|
530
|
+
proxyOf: [{ ...REAL_MOMOSTR_PROXY_OF[0], authoritative: false }],
|
|
531
|
+
}),
|
|
532
|
+
),
|
|
533
|
+
).toBeUndefined();
|
|
437
534
|
});
|
|
438
535
|
|
|
439
536
|
it('refuses a protocol the entry does not accept', () => {
|
|
440
|
-
expect(
|
|
441
|
-
|
|
442
|
-
|
|
537
|
+
expect(
|
|
538
|
+
derive(
|
|
539
|
+
candidate({
|
|
540
|
+
proxyOf: [
|
|
541
|
+
{ protocol: 'https://example.invalid/other', proxied: 'x', authoritative: true },
|
|
542
|
+
],
|
|
543
|
+
}),
|
|
544
|
+
),
|
|
545
|
+
).toBeUndefined();
|
|
443
546
|
});
|
|
444
547
|
|
|
445
548
|
it('can map the proxied identifier to a handle', () => {
|
|
@@ -447,9 +550,19 @@ describe('FEP-fffd proxyOf', () => {
|
|
|
447
550
|
protocols: ['https://atproto.com'],
|
|
448
551
|
handleFromProxied: (proxied) => proxied.replace(/^at:\/\//, '') || undefined,
|
|
449
552
|
});
|
|
450
|
-
expect(
|
|
451
|
-
|
|
452
|
-
|
|
553
|
+
expect(
|
|
554
|
+
mapped(
|
|
555
|
+
candidate({
|
|
556
|
+
proxyOf: [
|
|
557
|
+
{
|
|
558
|
+
protocol: 'https://atproto.com',
|
|
559
|
+
proxied: 'at://alice.bsky.social',
|
|
560
|
+
authoritative: true,
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
}),
|
|
564
|
+
),
|
|
565
|
+
).toBe('alice.bsky.social');
|
|
453
566
|
});
|
|
454
567
|
});
|
|
455
568
|
|
|
@@ -462,26 +575,35 @@ describe('FEP-fffd proxyOf', () => {
|
|
|
462
575
|
*/
|
|
463
576
|
it('re-labels nothing when the actor is on no listed host', () => {
|
|
464
577
|
const relabeller = createBridgeRelabeller([]);
|
|
465
|
-
expect(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
578
|
+
expect(
|
|
579
|
+
relabeller.deriveNetworkIdentity(
|
|
580
|
+
candidate({
|
|
581
|
+
host: 'attacker.example',
|
|
582
|
+
proxyOf: [{ protocol: 'https://x.example', proxied: 'elonmusk', authoritative: true }],
|
|
583
|
+
}),
|
|
584
|
+
),
|
|
585
|
+
).toBeUndefined();
|
|
469
586
|
});
|
|
470
587
|
|
|
471
588
|
it('re-labels nothing from an UNLISTED host even when a listed host uses the strategy', () => {
|
|
472
|
-
const relabeller = createBridgeRelabeller([
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
589
|
+
const relabeller = createBridgeRelabeller([
|
|
590
|
+
entry({
|
|
591
|
+
host: 'mirror.example',
|
|
592
|
+
derive: upstreamHandleFromProxyOf({ protocols: ['https://x.example'] }),
|
|
593
|
+
}),
|
|
594
|
+
]);
|
|
476
595
|
const claim = {
|
|
477
596
|
proxyOf: [{ protocol: 'https://x.example', proxied: 'elonmusk', authoritative: true }],
|
|
478
597
|
};
|
|
479
|
-
expect(
|
|
480
|
-
.
|
|
598
|
+
expect(
|
|
599
|
+
relabeller.deriveNetworkIdentity(candidate({ host: 'attacker.example', ...claim })),
|
|
600
|
+
).toBeUndefined();
|
|
481
601
|
// …and the very same claim from the REVIEWED host is honoured, which is
|
|
482
602
|
// what makes the previous assertion about trust rather than about parsing.
|
|
483
|
-
expect(
|
|
484
|
-
|
|
603
|
+
expect(
|
|
604
|
+
relabeller.deriveNetworkIdentity(candidate({ host: 'mirror.example', ...claim }))
|
|
605
|
+
?.federatedUsername,
|
|
606
|
+
).toBe('elonmusk@x.com');
|
|
485
607
|
});
|
|
486
608
|
});
|
|
487
609
|
});
|
|
@@ -518,16 +640,17 @@ describe('upstreamHandleFromAutomatedActor', () => {
|
|
|
518
640
|
},
|
|
519
641
|
);
|
|
520
642
|
|
|
521
|
-
it(
|
|
643
|
+
it("refuses an Application — that is the SERVER's own actor", () => {
|
|
522
644
|
// Mastodon publishes https://<host>/actor as an `Application` named
|
|
523
645
|
// `mastodon.internal`. Accepting it would re-label the instance actor onto
|
|
524
646
|
// the upstream network, which is a false attribution about the operator
|
|
525
647
|
// rather than about a person, but false all the same.
|
|
526
|
-
expect(
|
|
527
|
-
.
|
|
648
|
+
expect(
|
|
649
|
+
derive(candidate({ actorType: 'Application', preferredUsername: 'mastodon.internal' })),
|
|
650
|
+
).toBeUndefined();
|
|
528
651
|
});
|
|
529
652
|
|
|
530
|
-
it(
|
|
653
|
+
it("refuses a Person — the operator's own account is not a mirror", () => {
|
|
531
654
|
expect(derive(candidate({ actorType: 'Person', preferredUsername: 'admin' }))).toBeUndefined();
|
|
532
655
|
expect(derive(candidate({ actorType: 'Group' }))).toBeUndefined();
|
|
533
656
|
});
|
|
@@ -536,10 +659,14 @@ describe('upstreamHandleFromAutomatedActor', () => {
|
|
|
536
659
|
// The whole point: identity no longer depends on wording. A mirror with NO
|
|
537
660
|
// notice still resolves, and a Person carrying one still does not.
|
|
538
661
|
expect(derive(candidate({ bio: '' }))).toBe('PabloIglesias');
|
|
539
|
-
expect(
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
662
|
+
expect(
|
|
663
|
+
derive(
|
|
664
|
+
candidate({
|
|
665
|
+
actorType: 'Person',
|
|
666
|
+
bio: '(bot de x a mastodon administrado por mastox.eu, contacte con @admin)',
|
|
667
|
+
}),
|
|
668
|
+
),
|
|
669
|
+
).toBeUndefined();
|
|
543
670
|
});
|
|
544
671
|
|
|
545
672
|
it('refuses an actor with no preferredUsername rather than deriving an empty handle', () => {
|