@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.
Files changed (54) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/apUri.js +4 -2
  3. package/dist/cjs/httpSignature.js +1 -1
  4. package/dist/cjs/index.js +2 -2
  5. package/dist/cjs/networkIdentity.js +3 -2
  6. package/dist/cjs/node/actorResolver.js +24 -20
  7. package/dist/cjs/node/actorRouter.js +12 -4
  8. package/dist/cjs/node/delivery.js +7 -4
  9. package/dist/cjs/node/inboundDispatch.js +4 -3
  10. package/dist/cjs/node/index.js +7 -1
  11. package/dist/cjs/node/tcoLinks.js +97 -0
  12. package/dist/esm/.tsbuildinfo +1 -1
  13. package/dist/esm/apUri.js +4 -2
  14. package/dist/esm/httpSignature.js +1 -1
  15. package/dist/esm/index.js +3 -3
  16. package/dist/esm/networkIdentity.js +3 -2
  17. package/dist/esm/node/actorResolver.js +24 -20
  18. package/dist/esm/node/actorRouter.js +12 -4
  19. package/dist/esm/node/delivery.js +7 -4
  20. package/dist/esm/node/inboundDispatch.js +4 -3
  21. package/dist/esm/node/index.js +5 -0
  22. package/dist/esm/node/tcoLinks.js +94 -0
  23. package/dist/types/.tsbuildinfo +1 -1
  24. package/dist/types/httpSignature.d.ts +1 -1
  25. package/dist/types/index.d.ts +3 -3
  26. package/dist/types/node/delivery.d.ts +2 -3
  27. package/dist/types/node/index.d.ts +5 -0
  28. package/dist/types/node/tcoLinks.d.ts +41 -0
  29. package/package.json +2 -3
  30. package/src/__tests__/actorCollectionCounts.test.ts +27 -18
  31. package/src/__tests__/actorObject.test.ts +27 -7
  32. package/src/__tests__/actorResolver.test.ts +37 -22
  33. package/src/__tests__/actorResolverNetworkIdentity.test.ts +73 -36
  34. package/src/__tests__/apUri.test.ts +9 -5
  35. package/src/__tests__/delivery.test.ts +56 -24
  36. package/src/__tests__/federationHost.test.ts +13 -3
  37. package/src/__tests__/httpSignature.test.ts +1 -3
  38. package/src/__tests__/inboundDispatch.test.ts +80 -30
  39. package/src/__tests__/networkIdentity.test.ts +240 -113
  40. package/src/__tests__/routers.test.ts +87 -27
  41. package/src/__tests__/tcoLinks.test.ts +21 -0
  42. package/src/actorObject.ts +8 -3
  43. package/src/apUri.ts +6 -2
  44. package/src/httpSignature.ts +1 -1
  45. package/src/index.ts +8 -3
  46. package/src/networkIdentity.ts +14 -5
  47. package/src/node/actorResolver.ts +91 -50
  48. package/src/node/actorRouter.ts +38 -12
  49. package/src/node/delivery.ts +55 -21
  50. package/src/node/identityBridge.ts +38 -18
  51. package/src/node/inboundDispatch.ts +33 -13
  52. package/src/node/index.ts +11 -0
  53. package/src/node/signedFetch.ts +3 -1
  54. package/src/node/tcoLinks.ts +114 -0
@@ -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 / Mongoose): it depends only on
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.
@@ -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 Mongoose / Express / React
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
@@ -30,7 +30,7 @@ export { signRequest, verifyHttpSignature, HTTP_SIGNATURE_ALGORITHM, DEFAULT_SIG
30
30
  * Domain-parameterized ActivityPub URL builders — each app instantiates them once
31
31
  * with its own `FEDERATION_DOMAIN` so every actor stays `@user@its-own-domain`.
32
32
  */
33
- export { createUrlBuilders, normalizeActorUsername, INSTANCE_ACTOR_USERNAME, type UrlBuilders } from './urls';
33
+ export { createUrlBuilders, normalizeActorUsername, INSTANCE_ACTOR_USERNAME, type UrlBuilders, } from './urls';
34
34
  /**
35
35
  * Network identity: the MECHANISM for re-labelling an account republished by a
36
36
  * bridge onto the network it actually came from, plus the network vocabulary and
@@ -79,9 +79,8 @@ export interface DeliveryQueueJob {
79
79
  * The durable-delivery fallback (written when BullMQ is unavailable).
80
80
  *
81
81
  * App-supplied, like every other store in this package: the engine never names
82
- * a database. Mention backs it with Postgres; the method names below are the
83
- * ones its original Mongoose collection exposed and are kept only so the
84
- * adapter shape stays stable for consumers.
82
+ * a database. Mention backs it with Postgres; the method names below are kept
83
+ * stable so the adapter shape stays stable for consumers.
85
84
  */
86
85
  export interface DeliveryFallbackQueue {
87
86
  /** Insert one fallback delivery row (`queueDelivery`). */
@@ -17,6 +17,11 @@
17
17
  * actor + inbox + follow-graph Express routers.
18
18
  */
19
19
  export { createSignedFetch, type SignedFetch, type CreateSignedFetchConfig, type SingleHopFetch, type SingleHopFetchInit, type SignedFetchLogger, } from './signedFetch';
20
+ /**
21
+ * X's `t.co` wrapper unwrapped into the link the author posted, over the same
22
+ * injected single-hop transport — for bridged X bios and posts.
23
+ */
24
+ export { createTcoLinkExpander, type TcoLinkExpander, type TcoLinkCache, type CreateTcoLinkExpanderConfig, } from './tcoLinks';
20
25
  /**
21
26
  * The actor↔Oxy-user identity bridge — the default implementation of the
22
27
  * `PUT /users/resolve` + actor-gone archive/delete seam over an injected
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Expanding X's `t.co` link wrapper back into the link the author posted.
3
+ *
4
+ * X rewrites EVERY link in a post or bio into `https://t.co/<id>`, and a bridge
5
+ * that mirrors an X account (bird.makeup, mastox) republishes that text as it
6
+ * is: the actor `summary` and the Note `content` carry only the wrapper. The
7
+ * author never wrote `t.co`; it is X's click tracker standing in for their link,
8
+ * and an X API client never shows it because it reads the expansion beside it.
9
+ * The bridges drop that expansion, so the only place left to recover it is the
10
+ * wrapper's own redirect.
11
+ *
12
+ * Only `t.co` is unwrapped, and only ONE hop. A `bit.ly` the author chose is
13
+ * their link and stays; so does wherever their link redirects next. What is
14
+ * replaced is exactly X's wrapper, by exactly the URL it names.
15
+ *
16
+ * The transport is injected — the same SSRF-safe, IP-pinned single-hop fetch
17
+ * {@link createSignedFetch} takes — so each backend keeps its own network
18
+ * contract. A link that does not resolve (timeout, 404, a malformed or
19
+ * self-referencing `Location`) is left as `t.co`: still a working link, never a
20
+ * broken one.
21
+ */
22
+ import type { SingleHopFetch } from './signedFetch';
23
+ /**
24
+ * A store of resolved wrappers, keyed by the `t.co` id. A wrapper never changes
25
+ * what it points to, so an entry never goes stale. Failures are not cached.
26
+ */
27
+ export interface TcoLinkCache {
28
+ get(id: string): Promise<string | null | undefined>;
29
+ set(id: string, target: string): Promise<void>;
30
+ }
31
+ /** Adapters a {@link TcoLinkExpander} is built from. */
32
+ export interface CreateTcoLinkExpanderConfig {
33
+ /** SSRF-safe single-hop transport (does NOT follow redirects). */
34
+ fetchSingleHop: SingleHopFetch;
35
+ /** User-Agent presented to t.co. A browser UA gets an HTML page instead of the 301. */
36
+ userAgent: string;
37
+ cache?: TcoLinkCache;
38
+ }
39
+ /** Replace every `t.co` link in `text` with the URL it redirects to. */
40
+ export type TcoLinkExpander = (text: string) => Promise<string>;
41
+ export declare function createTcoLinkExpander(config: CreateTcoLinkExpanderConfig): TcoLinkExpander;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/federation",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Oxy Federation — the app-agnostic ActivityPub identity + follow engine substrate: the network-connector contract, normalized cross-network DTOs, HTTP signatures, actor resolution, the outbound delivery transport + follow lifecycle, the inbound dispatcher, and the webfinger/actor/inbox Express routers. Domain-parameterized so every Oxy app backend federates under its own domain.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -93,7 +93,7 @@
93
93
  }
94
94
  },
95
95
  "dependencies": {
96
- "@oxy.so/contracts": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0",
96
+ "@oxy.so/contracts": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || 4.11.0-oxy-one.0",
97
97
  "@oxy.so/core": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
98
98
  },
99
99
  "peerDependencies": {
@@ -105,7 +105,6 @@
105
105
  }
106
106
  },
107
107
  "devDependencies": {
108
- "@biomejs/biome": "^1.9.4",
109
108
  "@types/express": "^4.17.25",
110
109
  "@types/jest": "^30.0.0",
111
110
  "@types/node": "^22.20.1",
@@ -114,26 +114,32 @@ describe('fetchRemoteActor — remote collection counts', () => {
114
114
  expect(upserts[0]).toMatchObject({ followersCount: 1234, followingCount: 0, postsCount: 56 });
115
115
  });
116
116
 
117
- it.each([401, 403, 404, 410])('records a collection that answers %i as unknown (null), not 0', async (status) => {
118
- const { upserts } = await resolveWith({
119
- followers: new Response(null, { status }),
120
- following: collection(3),
121
- });
122
-
123
- expect(upserts[0]?.followersCount).toBeNull();
124
- expect(upserts[0]?.followingCount).toBe(3);
125
- });
117
+ it.each([401, 403, 404, 410])(
118
+ 'records a collection that answers %i as unknown (null), not 0',
119
+ async (status) => {
120
+ const { upserts } = await resolveWith({
121
+ followers: new Response(null, { status }),
122
+ following: collection(3),
123
+ });
124
+
125
+ expect(upserts[0]?.followersCount).toBeNull();
126
+ expect(upserts[0]?.followingCount).toBe(3);
127
+ },
128
+ );
126
129
 
127
130
  it.each([
128
131
  ['missing', undefined],
129
132
  ['a string', '1234'],
130
133
  ['negative', -1],
131
134
  ['fractional', 1.5],
132
- ])('records a collection whose totalItems is %s as unknown (null)', async (_label, totalItems) => {
133
- const { upserts } = await resolveWith({ followers: collection(totalItems) });
135
+ ])(
136
+ 'records a collection whose totalItems is %s as unknown (null)',
137
+ async (_label, totalItems) => {
138
+ const { upserts } = await resolveWith({ followers: collection(totalItems) });
134
139
 
135
- expect(upserts[0]?.followersCount).toBeNull();
136
- });
140
+ expect(upserts[0]?.followersCount).toBeNull();
141
+ },
142
+ );
137
143
 
138
144
  it('records an actor that advertises no collection as unknown (null)', async () => {
139
145
  const { upserts } = await resolveWith({
@@ -150,12 +156,15 @@ describe('fetchRemoteActor — remote collection counts', () => {
150
156
  ['a 429', () => Promise.resolve(new Response(null, { status: 429 }))],
151
157
  ['malformed JSON', () => Promise.resolve(new Response('{"totalItems": 12'))],
152
158
  ['an empty body', () => Promise.resolve(new Response(''))],
153
- ] as const)('OMITS a count whose fetch failed with %s, so a known value is kept', async (_label, followers) => {
154
- const { upserts } = await resolveWith({ followers, following: collection(9) });
159
+ ] as const)(
160
+ 'OMITS a count whose fetch failed with %s, so a known value is kept',
161
+ async (_label, followers) => {
162
+ const { upserts } = await resolveWith({ followers, following: collection(9) });
155
163
 
156
- expect(upserts[0]).not.toHaveProperty('followersCount');
157
- expect(upserts[0]?.followingCount).toBe(9);
158
- });
164
+ expect(upserts[0]).not.toHaveProperty('followersCount');
165
+ expect(upserts[0]?.followingCount).toBe(9);
166
+ },
167
+ );
159
168
 
160
169
  it('never hands the identity bridge a zero it did not read', async () => {
161
170
  const { bridged } = await resolveWith({
@@ -23,8 +23,10 @@ import {
23
23
 
24
24
  /** A media resolver that returns fixed absolute CDN URLs (avatar png, banner jpg). */
25
25
  const media: ActorMediaResolver = {
26
- resolveAvatar: (ref) => (ref === 'avatar-file-id' ? 'https://cloud.oxy.so/media/nate-avatar.png' : undefined),
27
- resolveBanner: (ref) => (ref === 'banner-file-id' ? 'https://cloud.oxy.so/media/nate-banner.jpg' : undefined),
26
+ resolveAvatar: (ref) =>
27
+ ref === 'avatar-file-id' ? 'https://cloud.oxy.so/media/nate-avatar.png' : undefined,
28
+ resolveBanner: (ref) =>
29
+ ref === 'banner-file-id' ? 'https://cloud.oxy.so/media/nate-banner.jpg' : undefined,
28
30
  };
29
31
 
30
32
  const buildActor = createLocalActorBuilder({
@@ -68,8 +70,16 @@ const EXPECTED_ACTOR: Record<string, unknown> = {
68
70
  endpoints: { sharedInbox: 'https://mention.earth/ap/inbox' },
69
71
  discoverable: true,
70
72
  manuallyApprovesFollowers: false,
71
- icon: { type: 'Image', url: 'https://cloud.oxy.so/media/nate-avatar.png', mediaType: 'image/png' },
72
- image: { type: 'Image', url: 'https://cloud.oxy.so/media/nate-banner.jpg', mediaType: 'image/jpeg' },
73
+ icon: {
74
+ type: 'Image',
75
+ url: 'https://cloud.oxy.so/media/nate-avatar.png',
76
+ mediaType: 'image/png',
77
+ },
78
+ image: {
79
+ type: 'Image',
80
+ url: 'https://cloud.oxy.so/media/nate-banner.jpg',
81
+ mediaType: 'image/jpeg',
82
+ },
73
83
  publicKey: {
74
84
  id: 'https://mention.earth/ap/users/nate#main-key',
75
85
  owner: 'https://mention.earth/ap/users/nate',
@@ -88,7 +98,9 @@ describe('createLocalActorBuilder (golden actor vector)', () => {
88
98
 
89
99
  it('serves the byte-identical document once wrapped in the route @context', () => {
90
100
  const served = { '@context': AP_CONTEXT, ...buildActor(PARAMS) };
91
- expect(JSON.stringify(served)).toBe(JSON.stringify({ '@context': AP_CONTEXT, ...EXPECTED_ACTOR }));
101
+ expect(JSON.stringify(served)).toBe(
102
+ JSON.stringify({ '@context': AP_CONTEXT, ...EXPECTED_ACTOR }),
103
+ );
92
104
  // publicKey.id host MUST equal the actor id host (Mastodon rejects a cross-domain key).
93
105
  const key = served.publicKey as { id: string };
94
106
  expect(new URL(key.id).host).toBe(new URL(served.id as string).host);
@@ -263,7 +275,13 @@ describe('createLocalActorBuilder — alsoKnownAs', () => {
263
275
  it('emits the aliases after publicKey, de-duplicated and https-only', () => {
264
276
  const actor = buildActor({
265
277
  ...PARAMS,
266
- alsoKnownAs: [alias, 'http://insecure.example/users/nate', 'not a url', alias, 'https://pleroma.example/users/n'],
278
+ alsoKnownAs: [
279
+ alias,
280
+ 'http://insecure.example/users/nate',
281
+ 'not a url',
282
+ alias,
283
+ 'https://pleroma.example/users/n',
284
+ ],
267
285
  });
268
286
  expect(actor.alsoKnownAs).toEqual([alias, 'https://pleroma.example/users/n']);
269
287
  const keys = Object.keys(actor);
@@ -271,7 +289,9 @@ describe('createLocalActorBuilder — alsoKnownAs', () => {
271
289
  });
272
290
 
273
291
  it('emits nothing when every alias is unpublishable', () => {
274
- expect(buildActor({ ...PARAMS, alsoKnownAs: ['http://x.example/u'] })).not.toHaveProperty('alsoKnownAs');
292
+ expect(buildActor({ ...PARAMS, alsoKnownAs: ['http://x.example/u'] })).not.toHaveProperty(
293
+ 'alsoKnownAs',
294
+ );
275
295
  });
276
296
  });
277
297
 
@@ -1,5 +1,9 @@
1
1
  import { createDomainPolicy } from '../apUri';
2
- import { createActorResolver, type ActorResolverConfig, type FederatedActorRecordBase } from '../node/actorResolver';
2
+ import {
3
+ createActorResolver,
4
+ type ActorResolverConfig,
5
+ type FederatedActorRecordBase,
6
+ } from '../node/actorResolver';
3
7
 
4
8
  /**
5
9
  * The instance domain policy applies to CACHED actors, not just fetched ones.
@@ -90,17 +94,23 @@ function makeResolver(
90
94
  * the work: a reader that stopped at the cap has pulled a handful of chunks; one
91
95
  * that drained has pulled all of them.
92
96
  */
93
- function streamedResponse(chunks: string[], init?: ResponseInit): { response: Response; pulls: () => number } {
97
+ function streamedResponse(
98
+ chunks: string[],
99
+ init?: ResponseInit,
100
+ ): { response: Response; pulls: () => number } {
94
101
  let pulls = 0;
95
102
  const encoder = new TextEncoder();
96
- const response = new Response(new ReadableStream<Uint8Array>({
97
- pull(controller) {
98
- pulls += 1;
99
- const chunk = chunks.shift();
100
- if (chunk === undefined) controller.close();
101
- else controller.enqueue(encoder.encode(chunk));
102
- },
103
- }), init);
103
+ const response = new Response(
104
+ new ReadableStream<Uint8Array>({
105
+ pull(controller) {
106
+ pulls += 1;
107
+ const chunk = chunks.shift();
108
+ if (chunk === undefined) controller.close();
109
+ else controller.enqueue(encoder.encode(chunk));
110
+ },
111
+ }),
112
+ init,
113
+ );
104
114
  return { response, pulls: () => pulls };
105
115
  }
106
116
 
@@ -176,7 +186,10 @@ describe('fetchRemoteActor — bounded remote bodies', () => {
176
186
  * stops at the cap pulls a handful; one that drains pulls all `count`.
177
187
  */
178
188
  function chunked(chunkBytes: number, count: number, init?: ResponseInit) {
179
- return streamedResponse(Array.from({ length: count }, () => 'x'.repeat(chunkBytes)), init);
189
+ return streamedResponse(
190
+ Array.from({ length: count }, () => 'x'.repeat(chunkBytes)),
191
+ init,
192
+ );
180
193
  }
181
194
 
182
195
  it('stops reading an actor body once it exceeds the decompressed-size cap', async () => {
@@ -210,14 +223,17 @@ describe('fetchRemoteActor — bounded remote bodies', () => {
210
223
 
211
224
  it('rejects an oversized actor from Content-Length without consuming its stream', async () => {
212
225
  let cancelled = false;
213
- const response = new Response(new ReadableStream<Uint8Array>({
214
- pull(controller) {
215
- controller.enqueue(new Uint8Array([123]));
216
- },
217
- cancel() {
218
- cancelled = true;
219
- },
220
- }), { headers: { 'content-length': String(1024 * 1024 + 1) } });
226
+ const response = new Response(
227
+ new ReadableStream<Uint8Array>({
228
+ pull(controller) {
229
+ controller.enqueue(new Uint8Array([123]));
230
+ },
231
+ cancel() {
232
+ cancelled = true;
233
+ },
234
+ }),
235
+ { headers: { 'content-length': String(1024 * 1024 + 1) } },
236
+ );
221
237
  const rig = makeResolver({ signedFetch: async () => response });
222
238
 
223
239
  await expect(rig.resolver.fetchRemoteActor(ALLOWED_ACTOR)).resolves.toBeNull();
@@ -233,9 +249,8 @@ describe('fetchRemoteActor — bounded remote bodies', () => {
233
249
  });
234
250
  const oversizedCollection = chunked(8 * 1024, 24);
235
251
  const rig = makeResolver({
236
- signedFetch: async (url) => url === ALLOWED_ACTOR
237
- ? new Response(actor)
238
- : oversizedCollection.response,
252
+ signedFetch: async (url) =>
253
+ url === ALLOWED_ACTOR ? new Response(actor) : oversizedCollection.response,
239
254
  });
240
255
 
241
256
  await expect(rig.resolver.fetchRemoteActor(ALLOWED_ACTOR)).resolves.toBeNull();
@@ -9,7 +9,11 @@
9
9
  * little and the account renders under a hostname that tells the reader nothing.
10
10
  */
11
11
 
12
- import { createActorResolver, type ActorResolverConfig, type FederatedActorRecordBase } from '../node/actorResolver';
12
+ import {
13
+ createActorResolver,
14
+ type ActorResolverConfig,
15
+ type FederatedActorRecordBase,
16
+ } from '../node/actorResolver';
13
17
  import {
14
18
  FEDERATION_NETWORKS,
15
19
  createBridgeRelabeller,
@@ -22,21 +26,26 @@ import {
22
26
  * supplies one — which is the arrangement in production too, only there the
23
27
  * entries are an app's reviewed policy.
24
28
  */
25
- const relabeller = createBridgeRelabeller([{
26
- host: 'bird.makeup',
27
- network: FEDERATION_NETWORKS.x,
28
- operator: 'test',
29
- software: 'BirdsiteLive',
30
- derive: upstreamHandleFromProfileField({ fieldName: 'Official', hosts: ['twitter.com', 'x.com'] }),
31
- caseRule: 'lowercase',
32
- relabel: 'enabled',
33
- upstreamIdStability: 'recyclable',
34
- boilerplate: [/\s*This account is a replica from Twitter\..*Patreon\.\s*$/s],
35
- consent: 'unconsented',
36
- evidence: 'test',
37
- assumption: '',
38
- since: '2026-08-02',
39
- }]);
29
+ const relabeller = createBridgeRelabeller([
30
+ {
31
+ host: 'bird.makeup',
32
+ network: FEDERATION_NETWORKS.x,
33
+ operator: 'test',
34
+ software: 'BirdsiteLive',
35
+ derive: upstreamHandleFromProfileField({
36
+ fieldName: 'Official',
37
+ hosts: ['twitter.com', 'x.com'],
38
+ }),
39
+ caseRule: 'lowercase',
40
+ relabel: 'enabled',
41
+ upstreamIdStability: 'recyclable',
42
+ boilerplate: [/\s*This account is a replica from Twitter\..*Patreon\.\s*$/s],
43
+ consent: 'unconsented',
44
+ evidence: 'test',
45
+ assumption: '',
46
+ since: '2026-08-02',
47
+ },
48
+ ]);
40
49
  const deriveBridgedNetworkIdentity = relabeller.deriveNetworkIdentity;
41
50
  import type { NormalizedExternalActor } from '../index';
42
51
 
@@ -51,13 +60,14 @@ const BRIDGED_ACTOR = {
51
60
  name: 'WIRED',
52
61
  inbox: 'https://bird.makeup/users/wired/inbox',
53
62
  summary:
54
- "The latest in tech.\nThis account is a replica from Twitter. Its author can't see your replies. "
55
- + 'If you find this service useful, please consider supporting us via our Patreon.',
63
+ "The latest in tech.\nThis account is a replica from Twitter. Its author can't see your replies. " +
64
+ 'If you find this service useful, please consider supporting us via our Patreon.',
56
65
  attachment: [
57
66
  {
58
67
  type: 'PropertyValue',
59
68
  name: 'Official',
60
- value: '<a href="https://twitter.com/wired" rel="me nofollow noopener noreferrer">twitter.com/wired</a>',
69
+ value:
70
+ '<a href="https://twitter.com/wired" rel="me nofollow noopener noreferrer">twitter.com/wired</a>',
61
71
  },
62
72
  ],
63
73
  };
@@ -75,8 +85,8 @@ const BRIDGED_ACTOR = {
75
85
  const ALL_BOILERPLATE_ACTOR = {
76
86
  ...BRIDGED_ACTOR,
77
87
  summary:
78
- "This account is a replica from Twitter. Its author can't see your replies. "
79
- + 'If you find this service useful, please consider supporting us via our Patreon.',
88
+ "This account is a replica from Twitter. Its author can't see your replies. " +
89
+ 'If you find this service useful, please consider supporting us via our Patreon.',
80
90
  };
81
91
 
82
92
  function makeResolver(
@@ -129,7 +139,12 @@ function makeResolver(
129
139
 
130
140
  ...textOverrides,
131
141
  },
132
- logger: { info: () => {}, warn: (message) => { warnings.push(message); } },
142
+ logger: {
143
+ info: () => {},
144
+ warn: (message) => {
145
+ warnings.push(message);
146
+ },
147
+ },
133
148
  };
134
149
 
135
150
  return { resolver: createActorResolver(config), upserts, resolved, warnings };
@@ -220,12 +235,21 @@ describe('deriveNetworkIdentity — results the engine refuses', () => {
220
235
  * swallowed.
221
236
  */
222
237
  it.each([
223
- ['username under a different domain', { federatedUsername: 'wired@example.com', instanceDomain: 'x.com', bio: '' }],
224
- ['username with no domain at all', { federatedUsername: 'wired', instanceDomain: 'x.com', bio: '' }],
238
+ [
239
+ 'username under a different domain',
240
+ { federatedUsername: 'wired@example.com', instanceDomain: 'x.com', bio: '' },
241
+ ],
242
+ [
243
+ 'username with no domain at all',
244
+ { federatedUsername: 'wired', instanceDomain: 'x.com', bio: '' },
245
+ ],
225
246
  ['empty local part', { federatedUsername: '@x.com', instanceDomain: 'x.com', bio: '' }],
226
247
  ['empty domain', { federatedUsername: 'wired@', instanceDomain: '', bio: '' }],
227
248
  ['two at-signs', { federatedUsername: 'a@b@x.com', instanceDomain: 'x.com', bio: '' }],
228
- ['a separator that is not an at-sign', { federatedUsername: 'wired.x.com', instanceDomain: 'x.com', bio: '' }],
249
+ [
250
+ 'a separator that is not an at-sign',
251
+ { federatedUsername: 'wired.x.com', instanceDomain: 'x.com', bio: '' },
252
+ ],
229
253
  ])('refuses %s and falls back to the protocol acct', async (_label, identity) => {
230
254
  const rig = makeResolver(() => identity);
231
255
  await rig.resolver.fetchRemoteActor(BRIDGED_ACTOR.id);
@@ -236,7 +260,11 @@ describe('deriveNetworkIdentity — results the engine refuses', () => {
236
260
  });
237
261
 
238
262
  it('accepts a bindable result that differs only in case', async () => {
239
- const rig = makeResolver(() => ({ federatedUsername: 'WIRED@X.com', instanceDomain: 'X.com', bio: 'x' }));
263
+ const rig = makeResolver(() => ({
264
+ federatedUsername: 'WIRED@X.com',
265
+ instanceDomain: 'X.com',
266
+ bio: 'x',
267
+ }));
240
268
  await rig.resolver.fetchRemoteActor(BRIDGED_ACTOR.id);
241
269
 
242
270
  expect(rig.resolved[0].federatedUsername).toBe('wired@x.com');
@@ -260,12 +288,17 @@ describe('bio handle qualification', () => {
260
288
  text.replace(/@([A-Za-z0-9_]+)(?!@)/g, `@$1@${domain}`);
261
289
 
262
290
  it('qualifies against the NETWORK domain for a relabelled actor', async () => {
263
- const rig = makeResolver(deriveBridgedNetworkIdentity, {
264
- ...BRIDGED_ACTOR,
265
- summary: 'Now building @thinkymachines'
266
- + "\nThis account is a replica from Twitter. Its author can't see your replies. "
267
- + 'If you find this service useful, please consider supporting us via our Patreon.',
268
- }, { qualifyHandles });
291
+ const rig = makeResolver(
292
+ deriveBridgedNetworkIdentity,
293
+ {
294
+ ...BRIDGED_ACTOR,
295
+ summary:
296
+ 'Now building @thinkymachines' +
297
+ "\nThis account is a replica from Twitter. Its author can't see your replies. " +
298
+ 'If you find this service useful, please consider supporting us via our Patreon.',
299
+ },
300
+ { qualifyHandles },
301
+ );
269
302
  await rig.resolver.fetchRemoteActor(BRIDGED_ACTOR.id);
270
303
 
271
304
  // x.com, NOT bird.makeup: the handle belongs to the network the account is
@@ -274,10 +307,14 @@ describe('bio handle qualification', () => {
274
307
  });
275
308
 
276
309
  it('writes the SAME qualified text to the stored row and to Oxy', async () => {
277
- const rig = makeResolver(deriveBridgedNetworkIdentity, {
278
- ...BRIDGED_ACTOR,
279
- summary: 'CTO @openai',
280
- }, { qualifyHandles });
310
+ const rig = makeResolver(
311
+ deriveBridgedNetworkIdentity,
312
+ {
313
+ ...BRIDGED_ACTOR,
314
+ summary: 'CTO @openai',
315
+ },
316
+ { qualifyHandles },
317
+ );
281
318
  await rig.resolver.fetchRemoteActor(BRIDGED_ACTOR.id);
282
319
 
283
320
  // The point of applying it once: two writes that could disagree, cannot.
@@ -28,8 +28,12 @@ describe('createDomainPolicy.isBlockedDomain', () => {
28
28
 
29
29
  describe('createDomainPolicy.extractLocalPostId', () => {
30
30
  it('extracts the post id from one of our own AP object URIs', () => {
31
- expect(policy.extractLocalPostId('https://mention.earth/ap/users/nate/posts/abc123')).toBe('abc123');
32
- expect(policy.extractLocalPostId('https://mention.earth/ap/users/nate/posts/abc123/')).toBe('abc123');
31
+ expect(policy.extractLocalPostId('https://mention.earth/ap/users/nate/posts/abc123')).toBe(
32
+ 'abc123',
33
+ );
34
+ expect(policy.extractLocalPostId('https://mention.earth/ap/users/nate/posts/abc123/')).toBe(
35
+ 'abc123',
36
+ );
33
37
  });
34
38
 
35
39
  it('returns null for a remote URI or a non-matching path', () => {
@@ -41,9 +45,9 @@ describe('createDomainPolicy.extractLocalPostId', () => {
41
45
 
42
46
  describe('extractActorUriFromActivityId', () => {
43
47
  it('trims from the first post-path segment to yield the actor uri', () => {
44
- expect(extractActorUriFromActivityId('https://mastodon.social/users/alice/statuses/12345')).toBe(
45
- 'https://mastodon.social/users/alice',
46
- );
48
+ expect(
49
+ extractActorUriFromActivityId('https://mastodon.social/users/alice/statuses/12345'),
50
+ ).toBe('https://mastodon.social/users/alice');
47
51
  });
48
52
 
49
53
  it('returns null when malformed or no post-path segment is present', () => {