@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
@@ -50,7 +50,8 @@ function makeWebfingerApp(overrides: Partial<WebfingerRouterConfig> = {}): Expre
50
50
  resolveUser: async (username) => (username === 'alice' ? { _id: 'u-alice' } : null),
51
51
  consent: {
52
52
  isSharingEnabledFromUser: () => true,
53
- getSharingStateByUsername: async (username) => (username === 'alice' ? 'enabled' : 'unknown-user'),
53
+ getSharingStateByUsername: async (username) =>
54
+ username === 'alice' ? 'enabled' : 'unknown-user',
54
55
  },
55
56
  cache: { get: async () => null, set: () => {} },
56
57
  logger: { error: () => {} },
@@ -60,10 +61,12 @@ function makeWebfingerApp(overrides: Partial<WebfingerRouterConfig> = {}): Expre
60
61
  return app;
61
62
  }
62
63
 
63
- function makeActorApp(overrides: {
64
- actorConfig?: Partial<ActorRouterConfig>;
65
- onDispatch?: (activity: Record<string, unknown>, verifiedActorUri: string) => void;
66
- } = {}): Express {
64
+ function makeActorApp(
65
+ overrides: {
66
+ actorConfig?: Partial<ActorRouterConfig>;
67
+ onDispatch?: (activity: Record<string, unknown>, verifiedActorUri: string) => void;
68
+ } = {},
69
+ ): Express {
67
70
  const app = express();
68
71
  app.use(
69
72
  express.json({
@@ -80,17 +83,30 @@ function makeActorApp(overrides: {
80
83
  apContentType: 'application/activity+json',
81
84
  urls,
82
85
  wantsActivityPub,
83
- getPublicKey: async () => ({ keyId: `${urls.actor('alice')}#main-key`, publicKeyPem: 'LOCALPEM' }),
84
- resolveUser: async (username) => (username === 'alice' ? { _id: 'u-alice', name: { displayName: 'Alice' }, _count: { followers: 3, following: 1 } } : null),
86
+ getPublicKey: async () => ({
87
+ keyId: `${urls.actor('alice')}#main-key`,
88
+ publicKeyPem: 'LOCALPEM',
89
+ }),
90
+ resolveUser: async (username) =>
91
+ username === 'alice'
92
+ ? { _id: 'u-alice', name: { displayName: 'Alice' }, _count: { followers: 3, following: 1 } }
93
+ : null,
85
94
  consent: {
86
95
  isSharingEnabledFromUser: () => true,
87
- getSharingStateByUsername: async (username) => (username === 'alice' ? 'enabled' : 'unknown-user'),
96
+ getSharingStateByUsername: async (username) =>
97
+ username === 'alice' ? 'enabled' : 'unknown-user',
88
98
  },
89
- buildLocalActorObject: (params) => ({ id: urls.actor(params.username), type: 'Person', name: params.displayName }),
99
+ buildLocalActorObject: (params) => ({
100
+ id: urls.actor(params.username),
101
+ type: 'Person',
102
+ name: params.displayName,
103
+ }),
90
104
  getBanner: async () => null,
91
105
  inbound: {
92
106
  fetchPublicKey: async (keyId) =>
93
- keyId === REMOTE_KEY_ID ? { publicKeyPem: REMOTE_PUBLIC_PEM, actorUri: REMOTE_ACTOR } : null,
107
+ keyId === REMOTE_KEY_ID
108
+ ? { publicKeyPem: REMOTE_PUBLIC_PEM, actorUri: REMOTE_ACTOR }
109
+ : null,
94
110
  trustForwardedHost: true,
95
111
  enqueueInboxActivity: async () => false, // force the inline dispatch path
96
112
  processInboxActivity: async (activity, verifiedActorUri) => {
@@ -107,24 +123,82 @@ function makeActorApp(overrides: {
107
123
  }
108
124
 
109
125
  describe('webfinger router', () => {
126
+ it('keeps the existing one-hour HTTP caching default', async () => {
127
+ const response = await request(makeWebfingerApp())
128
+ .get('/.well-known/webfinger')
129
+ .query({ resource: 'acct:alice@mention.earth' });
130
+ expect(response.headers['cache-control']).toBe('max-age=3600');
131
+ });
132
+
133
+ it.each([-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, 86401])(
134
+ 'rejects invalid HTTP cache TTL %s at construction',
135
+ (httpCacheTtlSeconds) => {
136
+ expect(() => makeWebfingerApp({ httpCacheTtlSeconds })).toThrow(RangeError);
137
+ },
138
+ );
139
+
140
+ it('makes fresh, cached and instance discovery noncacheable when TTL is zero', async () => {
141
+ const cacheGet = jest.fn(async () => null as { subject: string; links: [] } | null);
142
+ const app = makeWebfingerApp({
143
+ httpCacheTtlSeconds: 0,
144
+ cache: { get: cacheGet, set: () => {} },
145
+ });
146
+ const fresh = await request(app)
147
+ .get('/.well-known/webfinger')
148
+ .query({ resource: 'acct:alice@mention.earth' });
149
+ expect(fresh.status).toBe(200);
150
+ expect(fresh.headers['cache-control']).toBe('no-store');
151
+ cacheGet.mockResolvedValue({ subject: 'acct:alice@mention.earth', links: [] });
152
+ const cached = await request(app)
153
+ .get('/.well-known/webfinger')
154
+ .query({ resource: 'acct:alice@mention.earth' });
155
+ expect(cached.status).toBe(200);
156
+ expect(cached.headers['cache-control']).toBe('no-store');
157
+ expect(cached.body.links).toEqual([]);
158
+ const instance = await request(app)
159
+ .get('/.well-known/webfinger')
160
+ .query({ resource: 'acct:instance@mention.earth' });
161
+ expect(instance.status).toBe(200);
162
+ expect(instance.headers['cache-control']).toBe('no-store');
163
+ });
164
+
165
+ it('accepts the documented maximum HTTP cache TTL', async () => {
166
+ const response = await request(makeWebfingerApp({ httpCacheTtlSeconds: 86400 }))
167
+ .get('/.well-known/webfinger')
168
+ .query({ resource: 'acct:alice@mention.earth' });
169
+ expect(response.headers['cache-control']).toBe('max-age=86400');
170
+ });
171
+
110
172
  it('resolves acct:alice@mention.earth to the actor self link (200)', async () => {
111
- const res = await request(makeWebfingerApp()).get('/.well-known/webfinger').query({ resource: 'acct:alice@mention.earth' });
173
+ const res = await request(makeWebfingerApp())
174
+ .get('/.well-known/webfinger')
175
+ .query({ resource: 'acct:alice@mention.earth' });
112
176
  expect(res.status).toBe(200);
113
177
  expect(res.body.subject).toBe('acct:alice@mention.earth');
114
- expect(res.body.links[0]).toEqual({ rel: 'self', type: 'application/activity+json', href: urls.actor('alice') });
178
+ expect(res.body.links[0]).toEqual({
179
+ rel: 'self',
180
+ type: 'application/activity+json',
181
+ href: urls.actor('alice'),
182
+ });
115
183
  });
116
184
 
117
185
  it('normalizes mixed-case acct local-parts before resolve', async () => {
118
- const resolveUser = jest.fn(async (username: string) => (username === 'alice' ? { _id: 'u-alice' } : null));
186
+ const resolveUser = jest.fn(async (username: string) =>
187
+ username === 'alice' ? { _id: 'u-alice' } : null,
188
+ );
119
189
  const app = makeWebfingerApp({ resolveUser });
120
- const res = await request(app).get('/.well-known/webfinger').query({ resource: 'acct:Alice@mention.earth' });
190
+ const res = await request(app)
191
+ .get('/.well-known/webfinger')
192
+ .query({ resource: 'acct:Alice@mention.earth' });
121
193
  expect(res.status).toBe(200);
122
194
  expect(resolveUser).toHaveBeenCalledWith('alice');
123
195
  expect(res.body.subject).toBe('acct:alice@mention.earth');
124
196
  });
125
197
 
126
198
  it('404s an unknown domain', async () => {
127
- const res = await request(makeWebfingerApp()).get('/.well-known/webfinger').query({ resource: 'acct:alice@other.example' });
199
+ const res = await request(makeWebfingerApp())
200
+ .get('/.well-known/webfinger')
201
+ .query({ resource: 'acct:alice@other.example' });
128
202
  expect(res.status).toBe(404);
129
203
  });
130
204
 
@@ -138,9 +212,14 @@ describe('webfinger router', () => {
138
212
 
139
213
  it('404s a sharing-disabled user indistinguishably', async () => {
140
214
  const app = makeWebfingerApp({
141
- consent: { isSharingEnabledFromUser: () => false, getSharingStateByUsername: async () => 'disabled' },
215
+ consent: {
216
+ isSharingEnabledFromUser: () => false,
217
+ getSharingStateByUsername: async () => 'disabled',
218
+ },
142
219
  });
143
- const res = await request(app).get('/.well-known/webfinger').query({ resource: 'acct:alice@mention.earth' });
220
+ const res = await request(app)
221
+ .get('/.well-known/webfinger')
222
+ .query({ resource: 'acct:alice@mention.earth' });
144
223
  expect(res.status).toBe(404);
145
224
  expect(res.body).toEqual({ error: 'User not found' });
146
225
  });
@@ -193,9 +272,7 @@ describe('webfinger router — the instance (server) actor', () => {
193
272
  expect(webfinger.status).toBe(200);
194
273
  expect(actor.status).toBe(200);
195
274
 
196
- const selfLink = webfinger.body.links.find(
197
- (link: { rel: string }) => link.rel === 'self',
198
- );
275
+ const selfLink = webfinger.body.links.find((link: { rel: string }) => link.rel === 'self');
199
276
  expect(selfLink).toBeDefined();
200
277
  // Positive control: neither side may be vacuously undefined.
201
278
  expect(typeof actor.body.id).toBe('string');
@@ -298,6 +375,30 @@ describe('webfinger router — the instance (server) actor', () => {
298
375
  });
299
376
 
300
377
  describe('actor router — GET /ap/users/:username', () => {
378
+ it('keeps the existing half-hour actor caching default', async () => {
379
+ const response = await request(makeActorApp())
380
+ .get('/ap/users/alice')
381
+ .set('Accept', 'application/activity+json');
382
+ expect(response.status).toBe(200);
383
+ expect(response.headers['cache-control']).toBe('max-age=1800');
384
+ });
385
+ it.each(['alice', 'instance'])(
386
+ 'serves %s without HTTP caching when TTL is zero',
387
+ async (username) => {
388
+ const response = await request(makeActorApp({ actorConfig: { httpCacheTtlSeconds: 0 } }))
389
+ .get(`/ap/users/${username}`)
390
+ .set('Accept', 'application/activity+json');
391
+ expect(response.status).toBe(200);
392
+ expect(response.headers['cache-control']).toBe('no-store');
393
+ },
394
+ );
395
+ it.each([-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, 86401])(
396
+ 'rejects invalid actor cache TTL %s',
397
+ (httpCacheTtlSeconds) => {
398
+ expect(() => makeActorApp({ actorConfig: { httpCacheTtlSeconds } })).toThrow(RangeError);
399
+ },
400
+ );
401
+
301
402
  it('serves the actor as 200 (NOT a redirect) for an AP Accept', async () => {
302
403
  const res = await request(makeActorApp())
303
404
  .get('/ap/users/alice')
@@ -333,17 +434,34 @@ describe('actor router — GET /ap/users/:username', () => {
333
434
 
334
435
  describe('actor router — inbox POST', () => {
335
436
  /** Build a validly-signed inbox request body + headers for `activity`. */
336
- async function signedInbox(path: string, activity: Record<string, unknown>): Promise<{ body: string; headers: Record<string, string> }> {
437
+ async function signedInbox(
438
+ path: string,
439
+ activity: Record<string, unknown>,
440
+ ): Promise<{ body: string; headers: Record<string, string> }> {
337
441
  const body = JSON.stringify(activity);
338
- const headers = await signRequest(sign, REMOTE_KEY_ID, 'POST', `https://${DOMAIN}${path}`, body);
442
+ const headers = await signRequest(
443
+ sign,
444
+ REMOTE_KEY_ID,
445
+ 'POST',
446
+ `https://${DOMAIN}${path}`,
447
+ body,
448
+ );
339
449
  return { body, headers };
340
450
  }
341
451
 
342
452
  it('accepts a validly-signed activity (202) and reaches the dispatcher — no redirect', async () => {
343
453
  const dispatched: Array<{ activity: Record<string, unknown>; verifiedActorUri: string }> = [];
344
- const app = makeActorApp({ onDispatch: (activity, verifiedActorUri) => dispatched.push({ activity, verifiedActorUri }) });
454
+ const app = makeActorApp({
455
+ onDispatch: (activity, verifiedActorUri) => dispatched.push({ activity, verifiedActorUri }),
456
+ });
345
457
 
346
- const activity = { '@context': 'https://www.w3.org/ns/activitystreams', id: 'https://remote.example/f/1', type: 'Follow', actor: REMOTE_ACTOR, object: urls.actor('alice') };
458
+ const activity = {
459
+ '@context': 'https://www.w3.org/ns/activitystreams',
460
+ id: 'https://remote.example/f/1',
461
+ type: 'Follow',
462
+ actor: REMOTE_ACTOR,
463
+ object: urls.actor('alice'),
464
+ };
347
465
  const { body, headers } = await signedInbox('/ap/inbox', activity);
348
466
 
349
467
  const res = await request(app)
@@ -399,7 +517,9 @@ describe('actor router — inbox POST', () => {
399
517
  actorConfig: {
400
518
  inbound: {
401
519
  fetchPublicKey: async (keyId) =>
402
- keyId === REMOTE_KEY_ID ? { publicKeyPem: REMOTE_PUBLIC_PEM, actorUri: REMOTE_ACTOR } : null,
520
+ keyId === REMOTE_KEY_ID
521
+ ? { publicKeyPem: REMOTE_PUBLIC_PEM, actorUri: REMOTE_ACTOR }
522
+ : null,
403
523
  trustForwardedHost: true,
404
524
  enqueueInboxActivity: async () => false,
405
525
  processInboxActivity: dispatcher.processInboxActivity,
@@ -436,7 +556,12 @@ describe('actor router — inbox POST', () => {
436
556
  });
437
557
 
438
558
  it('rejects an actor mismatch with 403', async () => {
439
- const activity = { id: 'https://remote.example/f/2', type: 'Follow', actor: 'https://evil.example/users/x', object: urls.actor('alice') };
559
+ const activity = {
560
+ id: 'https://remote.example/f/2',
561
+ type: 'Follow',
562
+ actor: 'https://evil.example/users/x',
563
+ object: urls.actor('alice'),
564
+ };
440
565
  const { body, headers } = await signedInbox('/ap/inbox', activity);
441
566
  const res = await request(makeActorApp())
442
567
  .post('/ap/inbox')
@@ -448,7 +573,12 @@ describe('actor router — inbox POST', () => {
448
573
  });
449
574
 
450
575
  it('404s a user inbox for a sharing-disabled user (before signature verify)', async () => {
451
- const activity = { id: 'https://remote.example/f/3', type: 'Follow', actor: REMOTE_ACTOR, object: urls.actor('ghost') };
576
+ const activity = {
577
+ id: 'https://remote.example/f/3',
578
+ type: 'Follow',
579
+ actor: REMOTE_ACTOR,
580
+ object: urls.actor('ghost'),
581
+ };
452
582
  const { body, headers } = await signedInbox('/ap/users/ghost/inbox', activity);
453
583
  const res = await request(makeActorApp())
454
584
  .post('/ap/users/ghost/inbox')
@@ -0,0 +1,21 @@
1
+ import { createTcoLinkExpander } from '../node/tcoLinks';
2
+ import type { SingleHopFetch } from '../node/signedFetch';
3
+
4
+ const fetchSingleHop: SingleHopFetch = async (url) => {
5
+ const location = { 'https://t.co/abc123': 'https://example.com/', 'https://t.co/self12': url }[
6
+ url
7
+ ];
8
+ return location
9
+ ? new Response(null, { status: 301, headers: { location } })
10
+ : new Response(null, { status: 404 });
11
+ };
12
+
13
+ it('replaces a t.co wrapper with its target and leaves anything else as written', async () => {
14
+ const expand = createTcoLinkExpander({ fetchSingleHop, userAgent: 'test' });
15
+
16
+ await expect(
17
+ expand('a https://t.co/abc123 b https://t.co/gone12 c https://t.co/self12 d https://bit.ly/x'),
18
+ ).resolves.toBe(
19
+ 'a https://example.com/ b https://t.co/gone12 c https://t.co/self12 d https://bit.ly/x',
20
+ );
21
+ });
@@ -257,7 +257,9 @@ function buildActorIcon(
257
257
  if (!avatar) return undefined;
258
258
  const resolved = config.media.resolveAvatar(avatar);
259
259
  if (!resolved || !isAbsoluteHttpUrl(resolved)) {
260
- config.onWarn?.(`[Federation] Omitting actor icon — avatar did not resolve to an absolute URL (ref: ${avatar})`);
260
+ config.onWarn?.(
261
+ `[Federation] Omitting actor icon — avatar did not resolve to an absolute URL (ref: ${avatar})`,
262
+ );
261
263
  return undefined;
262
264
  }
263
265
  return apImageObject(resolved);
@@ -275,7 +277,9 @@ function buildActorImage(
275
277
  if (!banner) return undefined;
276
278
  const resolved = config.media.resolveBanner(banner);
277
279
  if (!resolved || !isAbsoluteHttpUrl(resolved)) {
278
- config.onWarn?.(`[Federation] Omitting actor image — banner did not resolve to an absolute URL (ref: ${banner})`);
280
+ config.onWarn?.(
281
+ `[Federation] Omitting actor image — banner did not resolve to an absolute URL (ref: ${banner})`,
282
+ );
279
283
  return undefined;
280
284
  }
281
285
  return apImageObject(resolved);
@@ -309,7 +313,8 @@ export function normalizeAlsoKnownAs(values: readonly string[] | null | undefine
309
313
  */
310
314
  export function createLocalActorBuilder(config: LocalActorBuilderConfig): LocalActorBuilder {
311
315
  return (params: BuildLocalActorParams): Record<string, unknown> => {
312
- const { username, displayName, kind, bio, avatar, profileHeaderImage, publicKey, createdAt } = params;
316
+ const { username, displayName, kind, bio, avatar, profileHeaderImage, publicKey, createdAt } =
317
+ params;
313
318
 
314
319
  const actorObject: Record<string, unknown> = {
315
320
  id: config.urls.actor(username),
package/src/apUri.ts CHANGED
@@ -135,7 +135,9 @@ export function createDomainPolicy(config: DomainPolicyConfig): DomainPolicy {
135
135
  canonicalFederationHost(config.domain),
136
136
  canonicalFederationHost(config.actorDomain ?? config.domain),
137
137
  ]);
138
- const identityApex = config.identityApex ? canonicalFederationHost(config.identityApex) : undefined;
138
+ const identityApex = config.identityApex
139
+ ? canonicalFederationHost(config.identityApex)
140
+ : undefined;
139
141
  const blocked = new Set<string>();
140
142
  for (const d of config.blockedDomains ?? []) {
141
143
  blocked.add(canonicalFederationHost(d));
@@ -144,7 +146,9 @@ export function createDomainPolicy(config: DomainPolicyConfig): DomainPolicy {
144
146
  return {
145
147
  isBlockedDomain(domain: string): boolean {
146
148
  const d = canonicalFederationHost(domain);
147
- return localDomains.has(d) || (identityApex !== undefined && d === identityApex) || blocked.has(d);
149
+ return (
150
+ localDomains.has(d) || (identityApex !== undefined && d === identityApex) || blocked.has(d)
151
+ );
148
152
  },
149
153
  extractLocalPostId(objectUri: string): string | null {
150
154
  let parsed: URL;
@@ -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.
package/src/index.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  * connector produces, the local-event union connectors deliver outbound, and
9
9
  * the connector interface itself.
10
10
  *
11
- * IMPORTANT: this entry is intentionally free of 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
@@ -43,7 +43,12 @@ export {
43
43
  * Domain-parameterized ActivityPub URL builders — each app instantiates them once
44
44
  * with its own `FEDERATION_DOMAIN` so every actor stays `@user@its-own-domain`.
45
45
  */
46
- export { createUrlBuilders, normalizeActorUsername, INSTANCE_ACTOR_USERNAME, type UrlBuilders } from './urls';
46
+ export {
47
+ createUrlBuilders,
48
+ normalizeActorUsername,
49
+ INSTANCE_ACTOR_USERNAME,
50
+ type UrlBuilders,
51
+ } from './urls';
47
52
 
48
53
  /**
49
54
  * Network identity: the MECHANISM for re-labelling an account republished by a
@@ -168,7 +168,8 @@ export function parseUpstreamProfileUrl(
168
168
 
169
169
  const host = canonicalFederationHost(url.hostname);
170
170
  for (const network of networks) {
171
- if (!network.profileHosts.some((allowed) => canonicalFederationHost(allowed) === host)) continue;
171
+ if (!network.profileHosts.some((allowed) => canonicalFederationHost(allowed) === host))
172
+ continue;
172
173
  const handle = profileUrlHandle(url.href, network.profileHosts, network.profilePathPrefix);
173
174
  if (handle !== undefined && handle.length > 0) return { network, handle };
174
175
  }
@@ -457,10 +458,16 @@ export function upstreamHandleFromProfileField(options: {
457
458
  // Observed bird.makeup Official assertion, 2026-09-13:
458
459
  // https://https://twitter.com/jordievole. Repair exactly one duplicated
459
460
  // HTTPS prefix, then run the ordinary host/path/credential validation.
460
- const candidateHref = options.repairRepeatedHttpsScheme && href.startsWith('https://https://')
461
- ? href.slice('https://'.length) : href;
461
+ const candidateHref =
462
+ options.repairRepeatedHttpsScheme && href.startsWith('https://https://')
463
+ ? href.slice('https://'.length)
464
+ : href;
462
465
  let handle: string | undefined;
463
- try { handle = profileUrlHandle(candidateHref, options.hosts, prefix); } catch { continue; }
466
+ try {
467
+ handle = profileUrlHandle(candidateHref, options.hosts, prefix);
468
+ } catch {
469
+ continue;
470
+ }
464
471
  if (handle !== undefined && handle.length > 0) handles.set(handle.toLowerCase(), handle);
465
472
  }
466
473
  }
@@ -681,7 +688,9 @@ export function createBridgeRelabeller(
681
688
  vouchesForNetwork: (actorHost, networkDomain) => {
682
689
  const bridge = findBridge(actorHost);
683
690
  if (!bridge) return false;
684
- return canonicalFederationHost(bridge.network.domain) === canonicalFederationHost(networkDomain);
691
+ return (
692
+ canonicalFederationHost(bridge.network.domain) === canonicalFederationHost(networkDomain)
693
+ );
685
694
  },
686
695
  deriveNetworkIdentity: (candidate) => {
687
696
  const entry = findBridge(candidate.host);