@oxy.so/federation 2.2.1 → 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.
Files changed (64) hide show
  1. package/README.md +31 -0
  2. package/dist/cjs/.tsbuildinfo +1 -1
  3. package/dist/cjs/apUri.js +4 -2
  4. package/dist/cjs/httpSignature.js +1 -1
  5. package/dist/cjs/index.js +2 -2
  6. package/dist/cjs/networkIdentity.js +3 -2
  7. package/dist/cjs/node/actorResolver.js +24 -20
  8. package/dist/cjs/node/actorRouter.js +16 -6
  9. package/dist/cjs/node/delivery.js +35 -10
  10. package/dist/cjs/node/httpCacheControl.js +11 -0
  11. package/dist/cjs/node/inboundDispatch.js +4 -3
  12. package/dist/cjs/node/index.js +8 -1
  13. package/dist/cjs/node/tcoLinks.js +97 -0
  14. package/dist/cjs/node/webfingerRouter.js +6 -4
  15. package/dist/esm/.tsbuildinfo +1 -1
  16. package/dist/esm/apUri.js +4 -2
  17. package/dist/esm/httpSignature.js +1 -1
  18. package/dist/esm/index.js +3 -3
  19. package/dist/esm/networkIdentity.js +3 -2
  20. package/dist/esm/node/actorResolver.js +24 -20
  21. package/dist/esm/node/actorRouter.js +16 -6
  22. package/dist/esm/node/delivery.js +33 -10
  23. package/dist/esm/node/httpCacheControl.js +8 -0
  24. package/dist/esm/node/inboundDispatch.js +4 -3
  25. package/dist/esm/node/index.js +6 -1
  26. package/dist/esm/node/tcoLinks.js +94 -0
  27. package/dist/esm/node/webfingerRouter.js +6 -4
  28. package/dist/types/.tsbuildinfo +1 -1
  29. package/dist/types/httpSignature.d.ts +1 -1
  30. package/dist/types/index.d.ts +3 -3
  31. package/dist/types/node/actorRouter.d.ts +2 -0
  32. package/dist/types/node/delivery.d.ts +23 -4
  33. package/dist/types/node/httpCacheControl.d.ts +2 -0
  34. package/dist/types/node/index.d.ts +6 -1
  35. package/dist/types/node/tcoLinks.d.ts +41 -0
  36. package/dist/types/node/webfingerRouter.d.ts +2 -0
  37. package/package.json +2 -3
  38. package/src/__tests__/actorCollectionCounts.test.ts +27 -18
  39. package/src/__tests__/actorObject.test.ts +27 -7
  40. package/src/__tests__/actorResolver.test.ts +37 -22
  41. package/src/__tests__/actorResolverNetworkIdentity.test.ts +73 -36
  42. package/src/__tests__/apUri.test.ts +9 -5
  43. package/src/__tests__/delivery.test.ts +165 -25
  44. package/src/__tests__/federationHost.test.ts +13 -3
  45. package/src/__tests__/httpSignature.test.ts +1 -3
  46. package/src/__tests__/inboundDispatch.test.ts +80 -30
  47. package/src/__tests__/networkIdentity.test.ts +240 -113
  48. package/src/__tests__/routers.test.ts +157 -27
  49. package/src/__tests__/tcoLinks.test.ts +21 -0
  50. package/src/actorObject.ts +8 -3
  51. package/src/apUri.ts +6 -2
  52. package/src/httpSignature.ts +1 -1
  53. package/src/index.ts +8 -3
  54. package/src/networkIdentity.ts +14 -5
  55. package/src/node/actorResolver.ts +91 -50
  56. package/src/node/actorRouter.ts +44 -14
  57. package/src/node/delivery.ts +112 -30
  58. package/src/node/httpCacheControl.ts +8 -0
  59. package/src/node/identityBridge.ts +38 -18
  60. package/src/node/inboundDispatch.ts +33 -13
  61. package/src/node/index.ts +13 -0
  62. package/src/node/signedFetch.ts +3 -1
  63. package/src/node/tcoLinks.ts +114 -0
  64. package/src/node/webfingerRouter.ts +8 -4
@@ -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({ fieldName: 'Official', hosts: ['twitter.com', 'x.com'] }),
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({ fieldName: 'Official', hosts: ['x.com'], requireRelMe: true });
66
- const fields = ['alice', 'bob'].map(handle => ({ name: 'Official', value: `<a href="https://x.com/${handle}" rel="me">Official</a>` }));
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(derive(candidate({ fields: [{ name: 'Official', value: '<a href="https://x.com/alice">Official</a>' }] }))).toBeUndefined();
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({ fields: [{ name: 'Official', value: '<a href="https://https://twitter.com/jordievole" rel="me">Official</a>' }] });
74
- expect(upstreamHandleFromProfileField({ fieldName: 'Official', hosts: ['twitter.com'] })(actor)).toBeUndefined();
75
- expect(upstreamHandleFromProfileField({ fieldName: 'Official', hosts: ['twitter.com'], repairRepeatedHttpsScheme: true })(actor)).toBe('jordievole');
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({ fields: [{ name: 'Official', value: `<a href="${href}" rel="me">Official</a>` }] });
88
- expect(upstreamHandleFromProfileField({ fieldName: 'Official', hosts: ['twitter.com'], repairRepeatedHttpsScheme: true })(actor)).toBeUndefined();
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(relabeller.deriveNetworkIdentity(candidate({ host: 'mastodon.social' }))).toBeUndefined();
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([entry({ caseRule: 'preserve' })])
108
- .deriveNetworkIdentity(candidate());
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
- ])('refuses a rule that yields %s, rather than collapsing a domain onto one identity', (_label, derived) => {
142
- // Asserted against the relabeller's OWN guard, with a rule that returns the
143
- // bad value directly. Driving it through `upstreamHandleFromPreferredUsername`
144
- // would prove nothing: that helper filters empties itself, so the outer guard
145
- // is never reached and the assertion passes with the guard deleted.
146
- const relabeller = createBridgeRelabeller([entry({ derive: () => derived })]);
147
- expect(relabeller.deriveNetworkIdentity(candidate())).toBeUndefined();
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(relabeller.deriveNetworkIdentity(candidate({ preferredUsername: ' ' }))).toBeUndefined();
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(relabeller.deriveNetworkIdentity(candidate({ preferredUsername: 'a@b' }))).toBeUndefined();
163
- expect(relabeller.deriveNetworkIdentity(candidate({ preferredUsername: 'a/b' }))).toBeUndefined();
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(relabeller.deriveNetworkIdentity(candidate({
169
- fields: [{ name: 'Official', value: '<a href="https://example.com/wired">x</a>' }],
170
- }))).toBeUndefined();
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(relabeller.deriveNetworkIdentity(candidate({
176
- fields: [{ name: 'Official', value: '<a href="https://twitter.com/i/status/1">x</a>' }],
177
- }))).toBeUndefined();
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({ derive: upstreamHandleFromProfileField({ fieldName: 'Official', hosts: ['www.twitter.com'] }) }),
241
+ entry({
242
+ derive: upstreamHandleFromProfileField({
243
+ fieldName: 'Official',
244
+ hosts: ['www.twitter.com'],
245
+ }),
246
+ }),
183
247
  ]);
184
- expect(relabeller.deriveNetworkIdentity(candidate({
185
- fields: [{ name: 'Official', value: '<a href="https://twitter.com/WIRED" rel="me">x</a>' }],
186
- proxyOf: [],
187
- }))?.federatedUsername).toBe('wired@x.com');
188
- expect(relabeller.deriveNetworkIdentity(candidate({
189
- fields: [{ name: 'Official', value: '<a href="https://www.twitter.com/WIRED" rel="me">x</a>' }],
190
- }))?.federatedUsername).toBe('wired@x.com');
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(relabeller.deriveNetworkIdentity(candidate({
208
- host: 'bsky.brid.gy',
209
- acct: 'jay.bsky.team@bsky.brid.gy',
210
- preferredUsername: 'jay.bsky.team',
211
- alsoKnownAs: [
212
- 'at://did:plc:abc123',
213
- 'https://bsky.app/profile/jay.bsky.team',
214
- ],
215
- fields: [],
216
- bio: '',
217
- }))?.federatedUsername).toBe('jay.bsky.team@bsky.social');
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(relabeller.deriveNetworkIdentity(candidate({ bio: 'I run this server.' }))).toBeUndefined();
227
- expect(relabeller.deriveNetworkIdentity(candidate({ bio: 'is a mirror bot.' }))?.federatedUsername)
228
- .toBe('wired@x.com');
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('strips a default handle\'s redundant suffix and keeps a custom domain whole', () => {
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
- .toBe('natgeo@instagram.com');
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('drops a default Bluesky handle\'s redundant suffix, exactly as ingest does', () => {
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(federatedUsernameFromUpstreamUrl('https://bsky.app/profile/georgemonbiot.bsky.social'))
339
- .toBe('georgemonbiot@bsky.social');
340
- expect(federatedUsernameFromUpstreamUrl('https://bsky.app/profile/gothamist.com'))
341
- .toBe('gothamist.com@bsky.social');
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([entry({
347
- host: 'mirror.example',
348
- network: FEDERATION_NETWORKS.x,
349
- derive: () => 'NASA',
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
- protocol: 'https://github.com/nostr-protocol/nostr',
371
- proxied: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
372
- authoritative: true,
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
- protocol: 'https://github.com/nostr-protocol/nostr',
379
- proxied: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
380
- authoritative: true,
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([{ protocol: 'p', proxied: 'q', authoritative: value }]);
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(readProxyDeclarations([
406
- { protocol: '', proxied: 'q' },
407
- { protocol: 'p', proxied: '' },
408
- { protocol: 'p' },
409
- 'not an object',
410
- null,
411
- [],
412
- ])).toEqual([]);
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(derive(candidate({
435
- proxyOf: [{ ...REAL_MOMOSTR_PROXY_OF[0], authoritative: false }],
436
- }))).toBeUndefined();
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(derive(candidate({
441
- proxyOf: [{ protocol: 'https://example.invalid/other', proxied: 'x', authoritative: true }],
442
- }))).toBeUndefined();
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(mapped(candidate({
451
- proxyOf: [{ protocol: 'https://atproto.com', proxied: 'at://alice.bsky.social', authoritative: true }],
452
- }))).toBe('alice.bsky.social');
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(relabeller.deriveNetworkIdentity(candidate({
466
- host: 'attacker.example',
467
- proxyOf: [{ protocol: 'https://x.example', proxied: 'elonmusk', authoritative: true }],
468
- }))).toBeUndefined();
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([entry({
473
- host: 'mirror.example',
474
- derive: upstreamHandleFromProxyOf({ protocols: ['https://x.example'] }),
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(relabeller.deriveNetworkIdentity(candidate({ host: 'attacker.example', ...claim })))
480
- .toBeUndefined();
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(relabeller.deriveNetworkIdentity(candidate({ host: 'mirror.example', ...claim }))
484
- ?.federatedUsername).toBe('elonmusk@x.com');
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('refuses an Application — that is the SERVER\'s own actor', () => {
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(derive(candidate({ actorType: 'Application', preferredUsername: 'mastodon.internal' })))
527
- .toBeUndefined();
648
+ expect(
649
+ derive(candidate({ actorType: 'Application', preferredUsername: 'mastodon.internal' })),
650
+ ).toBeUndefined();
528
651
  });
529
652
 
530
- it('refuses a Person — the operator\'s own account is not a mirror', () => {
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(derive(candidate({
540
- actorType: 'Person',
541
- bio: '(bot de x a mastodon administrado por mastox.eu, contacte con @admin)',
542
- }))).toBeUndefined();
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', () => {