@oxy.so/federation 1.0.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 (78) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +16 -0
  3. package/dist/cjs/.tsbuildinfo +1 -0
  4. package/dist/cjs/actorObject.js +216 -0
  5. package/dist/cjs/apContext.js +48 -0
  6. package/dist/cjs/apUri.js +132 -0
  7. package/dist/cjs/httpSignature.js +187 -0
  8. package/dist/cjs/index.js +99 -0
  9. package/dist/cjs/networkIdentity.js +487 -0
  10. package/dist/cjs/node/actorResolver.js +625 -0
  11. package/dist/cjs/node/actorRouter.js +307 -0
  12. package/dist/cjs/node/delivery.js +415 -0
  13. package/dist/cjs/node/identityBridge.js +133 -0
  14. package/dist/cjs/node/inboundDispatch.js +268 -0
  15. package/dist/cjs/node/index.js +63 -0
  16. package/dist/cjs/node/signedFetch.js +122 -0
  17. package/dist/cjs/node/webfingerRouter.js +166 -0
  18. package/dist/cjs/urls.js +55 -0
  19. package/dist/esm/.tsbuildinfo +1 -0
  20. package/dist/esm/actorObject.js +210 -0
  21. package/dist/esm/apContext.js +45 -0
  22. package/dist/esm/apUri.js +126 -0
  23. package/dist/esm/httpSignature.js +179 -0
  24. package/dist/esm/index.js +65 -0
  25. package/dist/esm/networkIdentity.js +472 -0
  26. package/dist/esm/node/actorResolver.js +620 -0
  27. package/dist/esm/node/actorRouter.js +304 -0
  28. package/dist/esm/node/delivery.js +412 -0
  29. package/dist/esm/node/identityBridge.js +130 -0
  30. package/dist/esm/node/inboundDispatch.js +263 -0
  31. package/dist/esm/node/index.js +51 -0
  32. package/dist/esm/node/signedFetch.js +119 -0
  33. package/dist/esm/node/webfingerRouter.js +163 -0
  34. package/dist/esm/urls.js +50 -0
  35. package/dist/types/.tsbuildinfo +1 -0
  36. package/dist/types/actorObject.d.ts +182 -0
  37. package/dist/types/apContext.d.ts +35 -0
  38. package/dist/types/apUri.d.ts +107 -0
  39. package/dist/types/httpSignature.d.ts +113 -0
  40. package/dist/types/index.d.ts +336 -0
  41. package/dist/types/networkIdentity.d.ts +509 -0
  42. package/dist/types/node/actorResolver.d.ts +287 -0
  43. package/dist/types/node/actorRouter.d.ts +108 -0
  44. package/dist/types/node/delivery.d.ts +248 -0
  45. package/dist/types/node/identityBridge.d.ts +84 -0
  46. package/dist/types/node/inboundDispatch.d.ts +156 -0
  47. package/dist/types/node/index.d.ts +51 -0
  48. package/dist/types/node/signedFetch.d.ts +74 -0
  49. package/dist/types/node/webfingerRouter.d.ts +62 -0
  50. package/dist/types/urls.d.ts +55 -0
  51. package/package.json +119 -0
  52. package/src/__tests__/actorObject.test.ts +258 -0
  53. package/src/__tests__/actorResolver.test.ts +252 -0
  54. package/src/__tests__/actorResolverNetworkIdentity.test.ts +297 -0
  55. package/src/__tests__/apUri.test.ts +53 -0
  56. package/src/__tests__/delivery.test.ts +432 -0
  57. package/src/__tests__/federationHost.test.ts +281 -0
  58. package/src/__tests__/httpSignature.test.ts +343 -0
  59. package/src/__tests__/inboundDispatch.test.ts +381 -0
  60. package/src/__tests__/index.test.ts +8 -0
  61. package/src/__tests__/networkIdentity.test.ts +525 -0
  62. package/src/__tests__/routers.test.ts +460 -0
  63. package/src/__tests__/urls.test.ts +26 -0
  64. package/src/actorObject.ts +313 -0
  65. package/src/apContext.ts +45 -0
  66. package/src/apUri.ts +161 -0
  67. package/src/httpSignature.ts +282 -0
  68. package/src/index.ts +419 -0
  69. package/src/networkIdentity.ts +731 -0
  70. package/src/node/actorResolver.ts +839 -0
  71. package/src/node/actorRouter.ts +438 -0
  72. package/src/node/delivery.ts +729 -0
  73. package/src/node/identityBridge.ts +230 -0
  74. package/src/node/inboundDispatch.ts +420 -0
  75. package/src/node/index.ts +136 -0
  76. package/src/node/signedFetch.ts +177 -0
  77. package/src/node/webfingerRouter.ts +226 -0
  78. package/src/urls.ts +71 -0
@@ -0,0 +1,525 @@
1
+ /**
2
+ * The network-identity MECHANISM. No bridge entries live in this package, so
3
+ * these drive the machinery with entries defined here in the test — which is
4
+ * also the point: an app supplies its own, and the mechanism must behave the
5
+ * same whatever they are.
6
+ *
7
+ * The entries an app actually ships, and the fixtures pinning their derivation
8
+ * rules against real actors, are tested beside those entries.
9
+ */
10
+
11
+ import {
12
+ FEDERATION_NETWORKS,
13
+ blueskyUsernameFromHandle,
14
+ createBridgeRelabeller,
15
+ parseUpstreamProfileUrl,
16
+ federatedUsernameFromUpstreamUrl,
17
+ stripBridgeBoilerplate,
18
+ upstreamHandleFromAutomatedActor,
19
+ upstreamHandleFromPreferredUsername,
20
+ upstreamHandleFromProfileField,
21
+ upstreamHandleFromProxyOf,
22
+ readProxyDeclarations,
23
+ upstreamHandleFromAlsoKnownAs,
24
+ upstreamProfileUrl,
25
+ type FederationBridgeEntry,
26
+ type NetworkIdentityCandidate,
27
+ } from '../networkIdentity';
28
+
29
+ function entry(overrides: Partial<FederationBridgeEntry> = {}): FederationBridgeEntry {
30
+ return {
31
+ host: 'mirror.example',
32
+ network: FEDERATION_NETWORKS.x,
33
+ operator: 'Test operator',
34
+ software: 'TestBridge',
35
+ derive: upstreamHandleFromProfileField({ fieldName: 'Official', hosts: ['twitter.com', 'x.com'] }),
36
+ caseRule: 'lowercase',
37
+ relabel: 'enabled',
38
+ upstreamIdStability: 'recyclable',
39
+ boilerplate: [/\s*Mirrored by mirror\.example\.\s*$/],
40
+ consent: 'unconsented',
41
+ evidence: 'test',
42
+ assumption: '',
43
+ since: '2026-08-02',
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ function candidate(overrides: Partial<NetworkIdentityCandidate> = {}): NetworkIdentityCandidate {
49
+ return {
50
+ host: 'mirror.example',
51
+ acct: 'wired@mirror.example',
52
+ preferredUsername: 'WIRED',
53
+ actorUri: 'https://mirror.example/users/WIRED',
54
+ actorType: 'Service',
55
+ alsoKnownAs: [],
56
+ fields: [{ name: 'Official', value: '<a href="https://twitter.com/WIRED" rel="me">x</a>' }],
57
+ proxyOf: [],
58
+ bio: 'The latest in tech.\nMirrored by mirror.example.',
59
+ ...overrides,
60
+ };
61
+ }
62
+
63
+ describe('createBridgeRelabeller', () => {
64
+ it('re-labels an actor onto the network its bridge mirrors', () => {
65
+ const identity = createBridgeRelabeller([entry()]).deriveNetworkIdentity(candidate());
66
+ expect(identity?.federatedUsername).toBe('wired@x.com');
67
+ expect(identity?.instanceDomain).toBe('x.com');
68
+ expect(identity?.bio).toBe('The latest in tech.');
69
+ });
70
+
71
+ it('declines an actor from a host no entry names', () => {
72
+ const relabeller = createBridgeRelabeller([entry()]);
73
+ expect(relabeller.deriveNetworkIdentity(candidate({ host: 'mastodon.social' }))).toBeUndefined();
74
+ });
75
+
76
+ it('ships no entries of its own — an empty registry re-labels nothing', () => {
77
+ expect(createBridgeRelabeller([]).deriveNetworkIdentity(candidate())).toBeUndefined();
78
+ });
79
+
80
+ it('preserves case where the entry says the handle is already canonical', () => {
81
+ const identity = createBridgeRelabeller([entry({ caseRule: 'preserve' })])
82
+ .deriveNetworkIdentity(candidate());
83
+ expect(identity?.federatedUsername).toBe('WIRED@x.com');
84
+ });
85
+ });
86
+
87
+ describe('createBridgeRelabeller — the pending_dedup gate', () => {
88
+ /**
89
+ * Re-labelling MANUFACTURES duplicates: two bridges of one network that render
90
+ * as visibly different accounts today both render the same handle afterwards.
91
+ * A `pending_dedup` entry is committed and reviewed but must stay inert.
92
+ */
93
+ it('does not re-label an entry that is pending de-duplication', () => {
94
+ const relabeller = createBridgeRelabeller([entry({ relabel: 'pending_dedup' })]);
95
+ expect(relabeller.deriveNetworkIdentity(candidate())).toBeUndefined();
96
+ });
97
+
98
+ it('still finds and vouches for a pending entry, so the trust question is separable', () => {
99
+ const relabeller = createBridgeRelabeller([entry({ relabel: 'pending_dedup' })]);
100
+ expect(relabeller.findBridge('mirror.example')?.relabel).toBe('pending_dedup');
101
+ expect(relabeller.vouchesForNetwork('mirror.example', 'x.com')).toBe(true);
102
+ });
103
+ });
104
+
105
+ describe('createBridgeRelabeller — derivations it refuses', () => {
106
+ /**
107
+ * An empty handle is the signature of a BROKEN rule, not an unusual account,
108
+ * and it is the most destructive outcome available: every actor on the domain
109
+ * would collapse onto one identity. We hold federated actors with no
110
+ * `preferredUsername` at all, so this is reachable rather than theoretical.
111
+ */
112
+ it.each([
113
+ ['an empty string', ''],
114
+ ['whitespace only', ' '],
115
+ ])('refuses a rule that yields %s, rather than collapsing a domain onto one identity', (_label, derived) => {
116
+ // Asserted against the relabeller's OWN guard, with a rule that returns the
117
+ // bad value directly. Driving it through `upstreamHandleFromPreferredUsername`
118
+ // would prove nothing: that helper filters empties itself, so the outer guard
119
+ // is never reached and the assertion passes with the guard deleted.
120
+ const relabeller = createBridgeRelabeller([entry({ derive: () => derived })]);
121
+ expect(relabeller.deriveNetworkIdentity(candidate())).toBeUndefined();
122
+ });
123
+
124
+ it('refuses an empty preferredUsername, which we hold real actors with', () => {
125
+ const relabeller = createBridgeRelabeller([
126
+ entry({ derive: upstreamHandleFromPreferredUsername([/./]) }),
127
+ ]);
128
+ expect(relabeller.deriveNetworkIdentity(candidate({ preferredUsername: '' }))).toBeUndefined();
129
+ expect(relabeller.deriveNetworkIdentity(candidate({ preferredUsername: ' ' }))).toBeUndefined();
130
+ });
131
+
132
+ it('refuses a handle carrying an at-sign or a slash', () => {
133
+ const relabeller = createBridgeRelabeller([
134
+ entry({ derive: upstreamHandleFromPreferredUsername([/./]) }),
135
+ ]);
136
+ expect(relabeller.deriveNetworkIdentity(candidate({ preferredUsername: 'a@b' }))).toBeUndefined();
137
+ expect(relabeller.deriveNetworkIdentity(candidate({ preferredUsername: 'a/b' }))).toBeUndefined();
138
+ });
139
+
140
+ it('derives nothing when the backlink points off the declared network', () => {
141
+ const relabeller = createBridgeRelabeller([entry()]);
142
+ expect(relabeller.deriveNetworkIdentity(candidate({
143
+ fields: [{ name: 'Official', value: '<a href="https://example.com/wired">x</a>' }],
144
+ }))).toBeUndefined();
145
+ });
146
+
147
+ it('derives nothing from a link that is not a bare profile path', () => {
148
+ const relabeller = createBridgeRelabeller([entry()]);
149
+ expect(relabeller.deriveNetworkIdentity(candidate({
150
+ fields: [{ name: 'Official', value: '<a href="https://twitter.com/i/status/1">x</a>' }],
151
+ }))).toBeUndefined();
152
+ });
153
+
154
+ it('matches profile hosts canonically, so a www. prefix on either side still round-trips', () => {
155
+ const relabeller = createBridgeRelabeller([
156
+ entry({ derive: upstreamHandleFromProfileField({ fieldName: 'Official', hosts: ['www.twitter.com'] }) }),
157
+ ]);
158
+ expect(relabeller.deriveNetworkIdentity(candidate({
159
+ fields: [{ name: 'Official', value: '<a href="https://twitter.com/WIRED" rel="me">x</a>' }],
160
+ proxyOf: [],
161
+ }))?.federatedUsername).toBe('wired@x.com');
162
+ expect(relabeller.deriveNetworkIdentity(candidate({
163
+ fields: [{ name: 'Official', value: '<a href="https://www.twitter.com/WIRED" rel="me">x</a>' }],
164
+ }))?.federatedUsername).toBe('wired@x.com');
165
+ });
166
+
167
+ it('derives a Bluesky handle from alsoKnownAs profile URLs (Bridgy Fed pattern)', () => {
168
+ const relabeller = createBridgeRelabeller([
169
+ entry({
170
+ host: 'bsky.brid.gy',
171
+ network: FEDERATION_NETWORKS.bluesky,
172
+ derive: (c) => {
173
+ const raw = upstreamHandleFromAlsoKnownAs({
174
+ hosts: ['bsky.app'],
175
+ pathPrefix: ['profile'],
176
+ })(c);
177
+ return raw === undefined ? undefined : blueskyUsernameFromHandle(raw);
178
+ },
179
+ }),
180
+ ]);
181
+ expect(relabeller.deriveNetworkIdentity(candidate({
182
+ host: 'bsky.brid.gy',
183
+ acct: 'jay.bsky.team@bsky.brid.gy',
184
+ preferredUsername: 'jay.bsky.team',
185
+ alsoKnownAs: [
186
+ 'at://did:plc:abc123',
187
+ 'https://bsky.app/profile/jay.bsky.team',
188
+ ],
189
+ fields: [],
190
+ bio: '',
191
+ }))?.federatedUsername).toBe('jay.bsky.team@bsky.social');
192
+ });
193
+
194
+ it('requires the marker before trusting a naming convention', () => {
195
+ // The bridge operator's own account lives on the same host and is not a
196
+ // mirror of anything; relabelling it would invent an upstream person.
197
+ const relabeller = createBridgeRelabeller([
198
+ entry({ derive: upstreamHandleFromPreferredUsername([/is a mirror bot\.$/]) }),
199
+ ]);
200
+ expect(relabeller.deriveNetworkIdentity(candidate({ bio: 'I run this server.' }))).toBeUndefined();
201
+ expect(relabeller.deriveNetworkIdentity(candidate({ bio: 'is a mirror bot.' }))?.federatedUsername)
202
+ .toBe('wired@x.com');
203
+ });
204
+ });
205
+
206
+ describe('createBridgeRelabeller — the trust predicate a resolver asks', () => {
207
+ it('lets a bridge vouch only for the network it mirrors', () => {
208
+ const relabeller = createBridgeRelabeller([entry()]);
209
+ expect(relabeller.vouchesForNetwork('mirror.example', 'x.com')).toBe(true);
210
+ expect(relabeller.vouchesForNetwork('mirror.example', 'instagram.com')).toBe(false);
211
+ expect(relabeller.vouchesForNetwork('other.example', 'x.com')).toBe(false);
212
+ expect(relabeller.vouchesForNetwork('', 'x.com')).toBe(false);
213
+ });
214
+
215
+ it('compares hosts canonically, so case and a www. prefix cannot slip past', () => {
216
+ const relabeller = createBridgeRelabeller([entry()]);
217
+ expect(relabeller.vouchesForNetwork('MIRROR.example', 'X.com')).toBe(true);
218
+ expect(relabeller.findBridge('www.mirror.example')?.host).toBe('mirror.example');
219
+ });
220
+ });
221
+
222
+ describe('boilerplate stripping', () => {
223
+ it('leaves a bio the pattern does not match exactly as written', () => {
224
+ const bio = 'A bio that mentions mirror.example but carries no notice.';
225
+ expect(stripBridgeBoilerplate(bio, entry())).toBe(bio);
226
+ });
227
+
228
+ it('strips every declared variant, so a multilingual notice is not half-removed', () => {
229
+ const multilingual = entry({ boilerplate: [/\s*\(bot\)\s*$/, /\s*\(robot\)\s*$/] });
230
+ expect(stripBridgeBoilerplate('Hola (robot)', multilingual)).toBe('Hola');
231
+ expect(stripBridgeBoilerplate('Hello (bot)', multilingual)).toBe('Hello');
232
+ });
233
+ });
234
+
235
+ describe('upstream profile URLs — one declaration, both directions', () => {
236
+ /**
237
+ * Rendering a link and recognising a pasted one are the same fact stated twice.
238
+ * Held as two tables they drift, and the drift lands on the parsing side, where
239
+ * a search that silently finds nothing looks exactly like "we do not have that
240
+ * account" — so it is asserted as a ROUND TRIP rather than in one direction.
241
+ */
242
+ it.each([
243
+ [FEDERATION_NETWORKS.x, 'nasa'],
244
+ [FEDERATION_NETWORKS.instagram, 'robert.habeck'],
245
+ [FEDERATION_NETWORKS.bluesky, 'georgemonbiot.bsky.social'],
246
+ ])('round-trips a $name handle', (network, handle) => {
247
+ const parsed = parseUpstreamProfileUrl(upstreamProfileUrl(network, handle));
248
+ expect(parsed?.network.id).toBe(network.id);
249
+ expect(parsed?.handle).toBe(handle);
250
+ });
251
+
252
+ it('recognises a network by its aliases, not only its canonical host', () => {
253
+ expect(parseUpstreamProfileUrl('https://twitter.com/nasa')?.network.id).toBe('x');
254
+ expect(parseUpstreamProfileUrl('https://x.com/nasa')?.network.id).toBe('x');
255
+ expect(parseUpstreamProfileUrl('https://mobile.x.com/nasa')?.network.id).toBe('x');
256
+ expect(parseUpstreamProfileUrl('https://www.instagram.com/nasa')?.network.id).toBe('instagram');
257
+ });
258
+
259
+ it('drops the tracking parameters a pasted URL usually carries', () => {
260
+ expect(parseUpstreamProfileUrl('https://x.com/nasa?s=20&t=abc')?.handle).toBe('nasa');
261
+ expect(parseUpstreamProfileUrl('https://x.com/nasa#bio')?.handle).toBe('nasa');
262
+ expect(parseUpstreamProfileUrl(' https://x.com/nasa ')?.handle).toBe('nasa');
263
+ });
264
+
265
+ it('answers undefined for anything that is not an upstream profile URL', () => {
266
+ expect(parseUpstreamProfileUrl('https://x.com/i/status/123')).toBeUndefined();
267
+ expect(parseUpstreamProfileUrl('https://x.com/')).toBeUndefined();
268
+ expect(parseUpstreamProfileUrl('https://mastodon.social/@alice')).toBeUndefined();
269
+ expect(parseUpstreamProfileUrl('not a url')).toBeUndefined();
270
+ expect(parseUpstreamProfileUrl('javascript:alert(1)')).toBeUndefined();
271
+ });
272
+
273
+ it('renders on the canonical host even when parsed from an alias', () => {
274
+ const parsed = parseUpstreamProfileUrl('https://twitter.com/nasa');
275
+ expect(parsed && upstreamProfileUrl(parsed.network, parsed.handle)).toBe('https://x.com/nasa');
276
+ });
277
+ });
278
+
279
+ describe('bsky.social is one network, whichever protocol an account arrives by', () => {
280
+ it('strips a default handle\'s redundant suffix and keeps a custom domain whole', () => {
281
+ expect(blueskyUsernameFromHandle('skylee1.bsky.social')).toBe('skylee1');
282
+ expect(blueskyUsernameFromHandle('gothamist.com')).toBe('gothamist.com');
283
+ expect(blueskyUsernameFromHandle('mayor.nyc.gov')).toBe('mayor.nyc.gov');
284
+ expect(blueskyUsernameFromHandle('jay.bsky.team')).toBe('jay.bsky.team');
285
+ expect(blueskyUsernameFromHandle('bsky.social')).toBe('bsky.social');
286
+ });
287
+ });
288
+
289
+ describe('federatedUsernameFromUpstreamUrl — the search direction', () => {
290
+ /**
291
+ * A pasted profile URL has to arrive at the SAME username the connector
292
+ * stored, or search returns nothing for an account we hold — a result
293
+ * indistinguishable from "we do not have that account", which is why nobody
294
+ * would ever report it.
295
+ */
296
+ it('resolves a pasted URL to the username Oxy stores', () => {
297
+ expect(federatedUsernameFromUpstreamUrl('https://x.com/nasa')).toBe('nasa@x.com');
298
+ expect(federatedUsernameFromUpstreamUrl('https://twitter.com/nasa')).toBe('nasa@x.com');
299
+ expect(federatedUsernameFromUpstreamUrl('https://mobile.x.com/nasa')).toBe('nasa@x.com');
300
+ expect(federatedUsernameFromUpstreamUrl('https://www.instagram.com/natgeo'))
301
+ .toBe('natgeo@instagram.com');
302
+ });
303
+
304
+ it('lowercases, because X and Instagram handles are case-insensitive', () => {
305
+ expect(federatedUsernameFromUpstreamUrl('https://x.com/NASA')).toBe('nasa@x.com');
306
+ expect(federatedUsernameFromUpstreamUrl('https://x.com/WIRED')).toBe('wired@x.com');
307
+ });
308
+
309
+ it('drops a default Bluesky handle\'s redundant suffix, exactly as ingest does', () => {
310
+ // The case a second, parallel parsing rule would get wrong: it works for X
311
+ // with plain lowercasing and silently fails here.
312
+ expect(federatedUsernameFromUpstreamUrl('https://bsky.app/profile/georgemonbiot.bsky.social'))
313
+ .toBe('georgemonbiot@bsky.social');
314
+ expect(federatedUsernameFromUpstreamUrl('https://bsky.app/profile/gothamist.com'))
315
+ .toBe('gothamist.com@bsky.social');
316
+ });
317
+
318
+ it('agrees with what the relabeller would store for the same account', () => {
319
+ // Ingest and search reading one declaration, asserted rather than assumed.
320
+ const relabeller = createBridgeRelabeller([entry({
321
+ host: 'mirror.example',
322
+ network: FEDERATION_NETWORKS.x,
323
+ derive: () => 'NASA',
324
+ })]);
325
+ const viaIngest = relabeller.deriveNetworkIdentity(candidate())?.federatedUsername;
326
+ expect(federatedUsernameFromUpstreamUrl('https://x.com/NASA')).toBe(viaIngest);
327
+ });
328
+
329
+ it('answers undefined for anything that is not an upstream profile URL', () => {
330
+ expect(federatedUsernameFromUpstreamUrl('https://mastodon.social/@alice')).toBeUndefined();
331
+ expect(federatedUsernameFromUpstreamUrl('https://x.com/i/status/1')).toBeUndefined();
332
+ expect(federatedUsernameFromUpstreamUrl('nasa')).toBeUndefined();
333
+ expect(federatedUsernameFromUpstreamUrl('')).toBeUndefined();
334
+ });
335
+ });
336
+
337
+ describe('FEP-fffd proxyOf', () => {
338
+ /**
339
+ * The exact bytes momostr.pink serves, fetched 2026-08-02. A Nostr bridge is
340
+ * the only thing in our corpus that publishes `proxyOf` at all, which is also
341
+ * why no shipped entry names this strategy.
342
+ */
343
+ const REAL_MOMOSTR_PROXY_OF = [{
344
+ protocol: 'https://github.com/nostr-protocol/nostr',
345
+ proxied: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
346
+ authoritative: true,
347
+ }];
348
+
349
+ describe('readProxyDeclarations', () => {
350
+ it('parses a real declaration off the wire', () => {
351
+ expect(readProxyDeclarations(REAL_MOMOSTR_PROXY_OF)).toEqual([{
352
+ protocol: 'https://github.com/nostr-protocol/nostr',
353
+ proxied: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
354
+ authoritative: true,
355
+ }]);
356
+ });
357
+
358
+ it('defaults authoritative to false when the actor omits it', () => {
359
+ // FEP-fffd leaves the flag optional, and it is the one bit that separates
360
+ // "this actor IS that account" from "this actor is a copy of it".
361
+ const [parsed] = readProxyDeclarations([{ protocol: 'p', proxied: 'q' }]);
362
+ expect(parsed.authoritative).toBe(false);
363
+ });
364
+
365
+ it('requires authoritative to be the boolean true, not merely truthy', () => {
366
+ // The remote actor writes this JSON, so it chooses the TYPE as well as the
367
+ // value. `"false"`, `1` and `{}` are all truthy, so a `Boolean(...)` read
368
+ // here would let an actor claim to BE an account while publishing
369
+ // something that reads, to a human, as a denial. `=== true` is what makes
370
+ // the flag mean what it says — and a strict check with no test for it is
371
+ // one refactor away from silently becoming the loose one.
372
+ for (const value of ['false', 'true', 1, {}, []]) {
373
+ const [parsed] = readProxyDeclarations([{ protocol: 'p', proxied: 'q', authoritative: value }]);
374
+ expect(parsed.authoritative).toBe(false);
375
+ }
376
+ });
377
+
378
+ it('drops malformed entries rather than inventing fields', () => {
379
+ expect(readProxyDeclarations([
380
+ { protocol: '', proxied: 'q' },
381
+ { protocol: 'p', proxied: '' },
382
+ { protocol: 'p' },
383
+ 'not an object',
384
+ null,
385
+ [],
386
+ ])).toEqual([]);
387
+ });
388
+
389
+ it('reads a missing or non-array proxyOf as none', () => {
390
+ expect(readProxyDeclarations(undefined)).toEqual([]);
391
+ expect(readProxyDeclarations({ protocol: 'p', proxied: 'q' })).toEqual([]);
392
+ });
393
+ });
394
+
395
+ describe('upstreamHandleFromProxyOf', () => {
396
+ const derive = upstreamHandleFromProxyOf({
397
+ protocols: ['https://github.com/nostr-protocol/nostr'],
398
+ });
399
+
400
+ it('reads the proxied identifier for a protocol the entry accepts', () => {
401
+ expect(derive(candidate({ proxyOf: REAL_MOMOSTR_PROXY_OF }))).toBe(
402
+ 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
403
+ );
404
+ });
405
+
406
+ it('refuses a non-authoritative declaration', () => {
407
+ // A copy does not get to stand in for the account it copied.
408
+ expect(derive(candidate({
409
+ proxyOf: [{ ...REAL_MOMOSTR_PROXY_OF[0], authoritative: false }],
410
+ }))).toBeUndefined();
411
+ });
412
+
413
+ it('refuses a protocol the entry does not accept', () => {
414
+ expect(derive(candidate({
415
+ proxyOf: [{ protocol: 'https://example.invalid/other', proxied: 'x', authoritative: true }],
416
+ }))).toBeUndefined();
417
+ });
418
+
419
+ it('can map the proxied identifier to a handle', () => {
420
+ const mapped = upstreamHandleFromProxyOf({
421
+ protocols: ['https://atproto.com'],
422
+ handleFromProxied: (proxied) => proxied.replace(/^at:\/\//, '') || undefined,
423
+ });
424
+ expect(mapped(candidate({
425
+ proxyOf: [{ protocol: 'https://atproto.com', proxied: 'at://alice.bsky.social', authoritative: true }],
426
+ }))).toBe('alice.bsky.social');
427
+ });
428
+ });
429
+
430
+ describe('it is gated by the registry, not applied globally', () => {
431
+ /**
432
+ * The load-bearing property. `proxyOf` is asserted by the untrusted actor
433
+ * itself, so honouring it wherever it appears would let ANY actor on ANY
434
+ * instance publish `proxied: "elonmusk"` and be stored, rendered and searched
435
+ * as that person. Only a host somebody reviewed can reach this strategy.
436
+ */
437
+ it('re-labels nothing when the actor is on no listed host', () => {
438
+ const relabeller = createBridgeRelabeller([]);
439
+ expect(relabeller.deriveNetworkIdentity(candidate({
440
+ host: 'attacker.example',
441
+ proxyOf: [{ protocol: 'https://x.example', proxied: 'elonmusk', authoritative: true }],
442
+ }))).toBeUndefined();
443
+ });
444
+
445
+ it('re-labels nothing from an UNLISTED host even when a listed host uses the strategy', () => {
446
+ const relabeller = createBridgeRelabeller([entry({
447
+ host: 'mirror.example',
448
+ derive: upstreamHandleFromProxyOf({ protocols: ['https://x.example'] }),
449
+ })]);
450
+ const claim = {
451
+ proxyOf: [{ protocol: 'https://x.example', proxied: 'elonmusk', authoritative: true }],
452
+ };
453
+ expect(relabeller.deriveNetworkIdentity(candidate({ host: 'attacker.example', ...claim })))
454
+ .toBeUndefined();
455
+ // …and the very same claim from the REVIEWED host is honoured, which is
456
+ // what makes the previous assertion about trust rather than about parsing.
457
+ expect(relabeller.deriveNetworkIdentity(candidate({ host: 'mirror.example', ...claim }))
458
+ ?.federatedUsername).toBe('elonmusk@x.com');
459
+ });
460
+ });
461
+ });
462
+
463
+ /**
464
+ * The mirror test that does not read prose.
465
+ *
466
+ * A bridge on stock server software has nothing to fingerprint, and the obvious
467
+ * fallback — matching the per-account notice it writes into each bio — fails on
468
+ * LANGUAGE. One deployment served that sentence in English, French and Spanish;
469
+ * an entry listing two of them left every account of the third under the
470
+ * bridge's hostname, notice still in the bio, looking like an ordinary account.
471
+ * `type` is the same claim in a machine-readable field.
472
+ */
473
+ describe('upstreamHandleFromAutomatedActor', () => {
474
+ const derive = upstreamHandleFromAutomatedActor();
475
+ const candidate = (over: Partial<NetworkIdentityCandidate>): NetworkIdentityCandidate => ({
476
+ host: 'mastox.eu',
477
+ acct: 'someone@mastox.eu',
478
+ preferredUsername: 'PabloIglesias',
479
+ actorUri: 'https://mastox.eu/users/PabloIglesias',
480
+ actorType: 'Service',
481
+ alsoKnownAs: [],
482
+ fields: [],
483
+ proxyOf: [],
484
+ bio: '',
485
+ ...over,
486
+ });
487
+
488
+ it.each(['Service', 'service', 'SERVICE'])(
489
+ 'accepts an actor published as %s, whatever its case',
490
+ (actorType) => {
491
+ expect(derive(candidate({ actorType }))).toBe('PabloIglesias');
492
+ },
493
+ );
494
+
495
+ it('refuses an Application — that is the SERVER\'s own actor', () => {
496
+ // Mastodon publishes https://<host>/actor as an `Application` named
497
+ // `mastodon.internal`. Accepting it would re-label the instance actor onto
498
+ // the upstream network, which is a false attribution about the operator
499
+ // rather than about a person, but false all the same.
500
+ expect(derive(candidate({ actorType: 'Application', preferredUsername: 'mastodon.internal' })))
501
+ .toBeUndefined();
502
+ });
503
+
504
+ it('refuses a Person — the operator\'s own account is not a mirror', () => {
505
+ expect(derive(candidate({ actorType: 'Person', preferredUsername: 'admin' }))).toBeUndefined();
506
+ expect(derive(candidate({ actorType: 'Group' }))).toBeUndefined();
507
+ });
508
+
509
+ it('does not read the bio at all, in any language', () => {
510
+ // The whole point: identity no longer depends on wording. A mirror with NO
511
+ // notice still resolves, and a Person carrying one still does not.
512
+ expect(derive(candidate({ bio: '' }))).toBe('PabloIglesias');
513
+ expect(derive(candidate({
514
+ actorType: 'Person',
515
+ bio: '(bot de x a mastodon administrado por mastox.eu, contacte con @admin)',
516
+ }))).toBeUndefined();
517
+ });
518
+
519
+ it('refuses an actor with no preferredUsername rather than deriving an empty handle', () => {
520
+ // An empty handle is the signature of a broken derivation and the most
521
+ // destructive outcome available — every actor on the host collapsing onto
522
+ // one identity.
523
+ expect(derive(candidate({ preferredUsername: ' ' }))).toBeUndefined();
524
+ });
525
+ });