@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
@@ -314,9 +314,11 @@ function sameOriginUrl(a: string, b: string): boolean {
314
314
  try {
315
315
  const urlA = new URL(a);
316
316
  const urlB = new URL(b);
317
- return urlA.protocol === urlB.protocol
318
- && urlA.port === urlB.port
319
- && isSameFederationHost(urlA.hostname, urlB.hostname);
317
+ return (
318
+ urlA.protocol === urlB.protocol &&
319
+ urlA.port === urlB.port &&
320
+ isSameFederationHost(urlA.hostname, urlB.hostname)
321
+ );
320
322
  } catch {
321
323
  return false;
322
324
  }
@@ -387,9 +389,7 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
387
389
  try {
388
390
  const data = await this.config.fetchWebFinger(url);
389
391
  if (!data) return null;
390
- const link = data.links?.find(
391
- (l) => l.rel === 'self' && isApActorContentType(l.type),
392
- );
392
+ const link = data.links?.find((l) => l.rel === 'self' && isApActorContentType(l.type));
393
393
  return link?.href || null;
394
394
  } catch (err) {
395
395
  this.config.logger.warn(`WebFinger resolution failed for ${acct}:`, err);
@@ -419,7 +419,9 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
419
419
  // throws here and is handled by the catch below.
420
420
  const requestedHost = new URL(currentUri).hostname.toLowerCase();
421
421
  if (this.config.isBlockedDomain(requestedHost)) {
422
- this.config.logger.info(`[FedSync] fetchRemoteActor skipping own/blocked domain ${requestedHost} for ${currentUri}`);
422
+ this.config.logger.info(
423
+ `[FedSync] fetchRemoteActor skipping own/blocked domain ${requestedHost} for ${currentUri}`,
424
+ );
423
425
  return null;
424
426
  }
425
427
 
@@ -431,20 +433,27 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
431
433
  // through to the WebFinger fallback (which recovers a STALE/wrong URI on a
432
434
  // transient failure, not a permanent removal). Only 410 does this.
433
435
  if (res.status === 410) {
434
- this.config.logger.info(`[FedSync] fetchRemoteActor 410 Gone for ${currentUri} — tombstoning actor`);
436
+ this.config.logger.info(
437
+ `[FedSync] fetchRemoteActor 410 Gone for ${currentUri} — tombstoning actor`,
438
+ );
435
439
  await this.tombstoneGoneActor(currentUri);
436
440
  return null;
437
441
  }
438
442
  const body = await readBoundedResponseBody(res, ERROR_BODY_MAX_BYTES).catch(() => '');
439
- this.config.logger.info(`[FedSync] fetchRemoteActor HTTP ${res.status} ${res.statusText} for ${currentUri} body=${body.slice(0, 500)}`);
443
+ this.config.logger.info(
444
+ `[FedSync] fetchRemoteActor HTTP ${res.status} ${res.statusText} for ${currentUri} body=${body.slice(0, 500)}`,
445
+ );
440
446
 
441
447
  // If direct fetch failed, try WebFinger to resolve the canonical actor URI.
442
448
  // Some servers (e.g., Threads) use numeric IDs in AP URIs that differ from
443
449
  // the username-based URI we may have stored.
444
450
  const parsed = new URL(currentUri);
445
451
  const pathUsername = parsed.pathname.split('/').filter(Boolean).pop();
446
- const acct = canonicalAcctHint
447
- || (pathUsername ? this.config.normalizeFederatedAcct(`${pathUsername}@${parsed.hostname}`) : undefined);
452
+ const acct =
453
+ canonicalAcctHint ||
454
+ (pathUsername
455
+ ? this.config.normalizeFederatedAcct(`${pathUsername}@${parsed.hostname}`)
456
+ : undefined);
448
457
  if (acct) {
449
458
  this.config.logger.info(`[FedSync] attempting WebFinger fallback for ${acct}`);
450
459
  const resolved = await this.resolveWebFinger(acct);
@@ -457,16 +466,24 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
457
466
  // A 410 on the WebFinger-RESOLVED URI is just as definitive. Tombstone
458
467
  // against the stored URI (not reassigned on this branch).
459
468
  if (res.status === 410) {
460
- this.config.logger.info(`[FedSync] fetchRemoteActor 410 Gone for resolved ${resolved} — tombstoning actor ${currentUri}`);
469
+ this.config.logger.info(
470
+ `[FedSync] fetchRemoteActor 410 Gone for resolved ${resolved} — tombstoning actor ${currentUri}`,
471
+ );
461
472
  await this.tombstoneGoneActor(currentUri);
462
473
  return null;
463
474
  }
464
- const body2 = await readBoundedResponseBody(res, ERROR_BODY_MAX_BYTES).catch(() => '');
465
- this.config.logger.info(`[FedSync] fetchRemoteActor HTTP ${res.status} for resolved ${resolved} body=${body2.slice(0, 500)}`);
475
+ const body2 = await readBoundedResponseBody(res, ERROR_BODY_MAX_BYTES).catch(
476
+ () => '',
477
+ );
478
+ this.config.logger.info(
479
+ `[FedSync] fetchRemoteActor HTTP ${res.status} for resolved ${resolved} body=${body2.slice(0, 500)}`,
480
+ );
466
481
  return null;
467
482
  }
468
483
  } else {
469
- this.config.logger.info(`[FedSync] WebFinger returned ${resolved ?? 'null'} for ${acct}`);
484
+ this.config.logger.info(
485
+ `[FedSync] WebFinger returned ${resolved ?? 'null'} for ${acct}`,
486
+ );
470
487
  return null;
471
488
  }
472
489
  } else {
@@ -478,42 +495,53 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
478
495
  const actorId = asString(actor.id);
479
496
  const actorInbox = asString(actor.inbox);
480
497
  if (!actorId || !actorInbox) {
481
- this.config.logger.info(`[FedSync] fetchRemoteActor missing fields for ${currentUri}: id=${!!actor.id} inbox=${!!actor.inbox} type=${String(actor.type)} keys=${Object.keys(actor).join(',')}`);
498
+ this.config.logger.info(
499
+ `[FedSync] fetchRemoteActor missing fields for ${currentUri}: id=${!!actor.id} inbox=${!!actor.inbox} type=${String(actor.type)} keys=${Object.keys(actor).join(',')}`,
500
+ );
482
501
  return null;
483
502
  }
484
503
 
485
504
  if (!sameOriginUrl(currentUri, actorId)) {
486
- this.config.logger.warn(`[FedSync] rejecting actor ${currentUri}: fetched URI is not authoritative for claimed id ${actorId}`);
505
+ this.config.logger.warn(
506
+ `[FedSync] rejecting actor ${currentUri}: fetched URI is not authoritative for claimed id ${actorId}`,
507
+ );
487
508
  return null;
488
509
  }
489
510
 
490
511
  if (!actorPublicKeyIsSelfConsistent(actor, actorId)) {
491
- this.config.logger.warn(`[FedSync] rejecting actor ${currentUri}: publicKey is not self-consistent for claimed id ${actorId}`);
512
+ this.config.logger.warn(
513
+ `[FedSync] rejecting actor ${currentUri}: publicKey is not self-consistent for claimed id ${actorId}`,
514
+ );
492
515
  return null;
493
516
  }
494
517
 
495
518
  const actorHost = new URL(actorId).hostname.toLowerCase();
496
- const username = this.config.text.inlineField(actor.preferredUsername)
497
- || this.config.text.inlineField(actor.name)
498
- || 'unknown';
499
- const actorWebfinger = typeof actor.webfinger === 'string'
500
- ? this.config.normalizeFederatedAcct(actor.webfinger)
501
- : undefined;
519
+ const username =
520
+ this.config.text.inlineField(actor.preferredUsername) ||
521
+ this.config.text.inlineField(actor.name) ||
522
+ 'unknown';
523
+ const actorWebfinger =
524
+ typeof actor.webfinger === 'string'
525
+ ? this.config.normalizeFederatedAcct(actor.webfinger)
526
+ : undefined;
502
527
  const verifiedAcctHint = this.acctMatchesActorHost(canonicalAcctHint, actorHost)
503
528
  ? canonicalAcctHint
504
529
  : undefined;
505
530
  const verifiedActorWebfinger = this.acctMatchesActorHost(actorWebfinger, actorHost)
506
531
  ? actorWebfinger
507
532
  : undefined;
508
- const acct = verifiedAcctHint
509
- || verifiedActorWebfinger
510
- || this.config.normalizeFederatedAcct(`${username}@${actorHost}`)
511
- || `${username.toLowerCase()}@${actorHost}`;
533
+ const acct =
534
+ verifiedAcctHint ||
535
+ verifiedActorWebfinger ||
536
+ this.config.normalizeFederatedAcct(`${username}@${actorHost}`) ||
537
+ `${username.toLowerCase()}@${actorHost}`;
512
538
  const domain = this.config.domainFromAcct(acct) || actorHost;
513
539
  // Re-check against the RESOLVED host/acct (post-redirect / WebFinger), which
514
540
  // can differ from the originally-requested URI host the early guard screened.
515
541
  if (this.config.isBlockedDomain(domain) || this.config.isBlockedDomain(actorHost)) {
516
- this.config.logger.info(`[FedSync] fetchRemoteActor blocked domain ${domain} actorHost=${actorHost} for ${currentUri}`);
542
+ this.config.logger.info(
543
+ `[FedSync] fetchRemoteActor blocked domain ${domain} actorHost=${actorHost} for ${currentUri}`,
544
+ );
517
545
  return null;
518
546
  }
519
547
 
@@ -536,9 +564,10 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
536
564
  const attRecord = asRecord(att);
537
565
  if (!attRecord || attRecord.type !== 'PropertyValue') continue;
538
566
  const fieldName = this.config.text.inlineField(attRecord.name);
539
- const fieldValue = typeof attRecord.value === 'string'
540
- ? this.config.text.sanitizeFieldValue(attRecord.value)
541
- : '';
567
+ const fieldValue =
568
+ typeof attRecord.value === 'string'
569
+ ? this.config.text.sanitizeFieldValue(attRecord.value)
570
+ : '';
542
571
  if (!fieldName || !fieldValue) continue;
543
572
  fields.push({
544
573
  name: fieldName,
@@ -552,7 +581,8 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
552
581
  const headerUrl = this.config.firstStringUrl(actor.image);
553
582
  // `summary` is the actor's bio — a BODY, so its line breaks are the author's
554
583
  // and must survive; `htmlToPlainText` normalizes it as multiline.
555
- const summary = typeof actor.summary === 'string' ? this.config.text.htmlToPlainText(actor.summary) : '';
584
+ const summary =
585
+ typeof actor.summary === 'string' ? this.config.text.htmlToPlainText(actor.summary) : '';
556
586
  // The display name is one line. Entity-decode FIRST (an encoded `&#10;` or
557
587
  // `&nbsp;` only becomes whitespace once decoded), THEN collapse.
558
588
  const rawDisplayName = typeof actor.name === 'string' ? actor.name : '';
@@ -617,7 +647,8 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
617
647
  featuredTagsUrl: asString(actor.featuredTags) || undefined,
618
648
  alsoKnownAs,
619
649
  networkAcct: networkIdentity?.federatedUsername,
620
- remoteCreatedAt: typeof actor.published === 'string' ? new Date(actor.published) : undefined,
650
+ remoteCreatedAt:
651
+ typeof actor.published === 'string' ? new Date(actor.published) : undefined,
621
652
  // Omitted, not written as `undefined`, when this refresh could not tell:
622
653
  // an absent key is what tells the store to keep the value it has.
623
654
  ...(followersCount !== undefined && { followersCount }),
@@ -663,7 +694,9 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
663
694
  postsCount: postsCount ?? undefined,
664
695
  oxyUserId: fedActor.oxyUserId ?? undefined,
665
696
  };
666
- const oxyId = await this.config.identity.resolveExternalUser(normalized, { forceAvatarRefresh });
697
+ const oxyId = await this.config.identity.resolveExternalUser(normalized, {
698
+ forceAvatarRefresh,
699
+ });
667
700
  if (oxyId && fedActor.oxyUserId !== oxyId && fedActor._id != null) {
668
701
  await this.config.store.setActorOxyUserId(fedActor._id, oxyId);
669
702
  }
@@ -691,9 +724,7 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
691
724
  * actor. The refusal is logged: it is a bug in the app's rule, not a normal
692
725
  * outcome, and it must not pass silently.
693
726
  */
694
- private resolveNetworkIdentity(
695
- candidate: NetworkIdentityCandidate,
696
- ): NetworkIdentity | undefined {
727
+ private resolveNetworkIdentity(candidate: NetworkIdentityCandidate): NetworkIdentity | undefined {
697
728
  const derived = this.config.deriveNetworkIdentity?.(candidate);
698
729
  if (!derived) return undefined;
699
730
 
@@ -707,13 +738,13 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
707
738
  const atIndex = federatedUsername.indexOf('@');
708
739
  const localPart = atIndex > 0 ? federatedUsername.slice(0, atIndex) : '';
709
740
  if (
710
- domain.length === 0
711
- || localPart.length === 0
712
- || federatedUsername !== `${localPart}@${domain}`
741
+ domain.length === 0 ||
742
+ localPart.length === 0 ||
743
+ federatedUsername !== `${localPart}@${domain}`
713
744
  ) {
714
745
  this.config.logger.warn(
715
- `[FedSync] refusing network identity for ${candidate.actorUri}: `
716
- + `"${derived.federatedUsername}" is not bindable to domain "${derived.instanceDomain}"`,
746
+ `[FedSync] refusing network identity for ${candidate.actorUri}: ` +
747
+ `"${derived.federatedUsername}" is not bindable to domain "${derived.instanceDomain}"`,
717
748
  );
718
749
  return undefined;
719
750
  }
@@ -733,7 +764,9 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
733
764
  try {
734
765
  const actor = await this.config.store.tombstoneActor(actorUri);
735
766
  if (!actor) {
736
- this.config.logger.info(`[FedSync] 410 Gone for ${actorUri} — no stored actor row to tombstone`);
767
+ this.config.logger.info(
768
+ `[FedSync] 410 Gone for ${actorUri} — no stored actor row to tombstone`,
769
+ );
737
770
  return;
738
771
  }
739
772
 
@@ -741,7 +774,9 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
741
774
 
742
775
  if (actor.oxyUserId) {
743
776
  const outcome = await this.config.identity.reportActorGone(actor.oxyUserId);
744
- this.config.logger.info(`[FedSync] actor-gone report for ${actorUri} (oxyUserId ${actor.oxyUserId}) → ${outcome}`);
777
+ this.config.logger.info(
778
+ `[FedSync] actor-gone report for ${actorUri} (oxyUserId ${actor.oxyUserId}) → ${outcome}`,
779
+ );
745
780
  }
746
781
  } catch (err) {
747
782
  this.config.logger.warn(`[FedSync] failed to tombstone gone actor ${actorUri}:`, err);
@@ -793,12 +828,15 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
793
828
  // instance that has ever reached us), blocking its domain changed nothing. That
794
829
  // made the blocklist inert for exactly the hosts it is added for.
795
830
  if (this.isBlockedActorUri(actorUri)) {
796
- this.config.logger.info(`[FedSync] getOrFetchActor refusing own/blocked domain for ${actorUri}`);
831
+ this.config.logger.info(
832
+ `[FedSync] getOrFetchActor refusing own/blocked domain for ${actorUri}`,
833
+ );
797
834
  return null;
798
835
  }
799
836
  const existing = await this.config.store.findActorByUri(actorUri);
800
837
  if (existing) {
801
- const isStale = !existing.lastFetchedAt || Date.now() - existing.lastFetchedAt.getTime() > ACTOR_STALE_MS;
838
+ const isStale =
839
+ !existing.lastFetchedAt || Date.now() - existing.lastFetchedAt.getTime() > ACTOR_STALE_MS;
802
840
  if (isStale) {
803
841
  // Refresh in the background — never block the caller on remote I/O.
804
842
  this.refreshActorInBackground(actorUri, existing);
@@ -820,8 +858,9 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
820
858
 
821
859
  const missingProfile = !existing || !existing.avatarUrl || !existing.headerUrl;
822
860
  const lastFetchedMs = existing?.lastFetchedAt?.getTime();
823
- const refreshedRecently = typeof lastFetchedMs === 'number'
824
- && Date.now() - lastFetchedMs < ACTOR_REFRESH_MIN_INTERVAL_MS;
861
+ const refreshedRecently =
862
+ typeof lastFetchedMs === 'number' &&
863
+ Date.now() - lastFetchedMs < ACTOR_REFRESH_MIN_INTERVAL_MS;
825
864
 
826
865
  // Skip if we refreshed recently AND the cached profile is already complete.
827
866
  if (refreshedRecently && !missingProfile) return;
@@ -835,7 +874,9 @@ export class ActorResolver<TActor extends FederatedActorRecordBase> {
835
874
  await this.fetchRemoteActor(actorUri, forceAvatarRefresh, existing?.acct);
836
875
  } catch (err) {
837
876
  const message = err instanceof Error ? err.message : String(err);
838
- this.config.logger.warn(`[FedSync] background actor refresh failed for ${actorUri}: ${message}`);
877
+ this.config.logger.warn(
878
+ `[FedSync] background actor refresh failed for ${actorUri}: ${message}`,
879
+ );
839
880
  } finally {
840
881
  this.inFlightActorRefreshes.delete(actorUri);
841
882
  }
@@ -104,9 +104,15 @@ export interface ActorRouterConfig {
104
104
  /** Whether to trust `X-Forwarded-Host` when reconstructing the signed host line. */
105
105
  trustForwardedHost: boolean;
106
106
  /** Enqueue a verified inbound activity for async processing (false ⇒ process inline). */
107
- enqueueInboxActivity(job: { activity: Record<string, unknown>; verifiedActorUri: string }): Promise<boolean>;
107
+ enqueueInboxActivity(job: {
108
+ activity: Record<string, unknown>;
109
+ verifiedActorUri: string;
110
+ }): Promise<boolean>;
108
111
  /** The inbound dispatcher (the inline-fallback + post-enqueue processor). */
109
- processInboxActivity(activity: Record<string, unknown>, verifiedActorUri: string): Promise<void>;
112
+ processInboxActivity(
113
+ activity: Record<string, unknown>,
114
+ verifiedActorUri: string,
115
+ ): Promise<void>;
110
116
  };
111
117
  /** Fetch one page of a user's Oxy follow graph (followers OR following). */
112
118
  fetchFollowPage(
@@ -161,7 +167,11 @@ export function createActorRouter(config: ActorRouterConfig): Router {
161
167
  async function handleInbox(req: InboxRequest, res: Response): Promise<Response> {
162
168
  try {
163
169
  // Verify HTTP signature (use originalUrl to avoid proxy path mangling).
164
- const { verified, actorUri, reason: signatureError } = await verifyHttpSignature(
170
+ const {
171
+ verified,
172
+ actorUri,
173
+ reason: signatureError,
174
+ } = await verifyHttpSignature(
165
175
  {
166
176
  method: req.method,
167
177
  path: req.originalUrl || req.path,
@@ -186,7 +196,8 @@ export function createActorRouter(config: ActorRouterConfig): Router {
186
196
  }
187
197
 
188
198
  // Verify the actor in the activity matches the signature.
189
- const activityActor = typeof activity.actor === 'string' ? activity.actor : activity.actor?.id;
199
+ const activityActor =
200
+ typeof activity.actor === 'string' ? activity.actor : activity.actor?.id;
190
201
  if (activityActor !== actorUri) {
191
202
  logger.debug(`Inbox: Actor mismatch. Signed: ${actorUri}, Activity: ${activityActor}`);
192
203
  return res.status(403).json({ error: 'Actor mismatch' });
@@ -199,7 +210,10 @@ export function createActorRouter(config: ActorRouterConfig): Router {
199
210
  // is never dropped.
200
211
  let enqueued = false;
201
212
  try {
202
- enqueued = await config.inbound.enqueueInboxActivity({ activity, verifiedActorUri: actorUri });
213
+ enqueued = await config.inbound.enqueueInboxActivity({
214
+ activity,
215
+ verifiedActorUri: actorUri,
216
+ });
203
217
  } catch (err) {
204
218
  logger.error('Failed to enqueue inbox activity — processing inline:', err);
205
219
  enqueued = false;
@@ -243,7 +257,8 @@ export function createActorRouter(config: ActorRouterConfig): Router {
243
257
 
244
258
  const userId = String(user._id || user.id);
245
259
 
246
- const rawCount: unknown = direction === 'followers' ? user._count?.followers : user._count?.following;
260
+ const rawCount: unknown =
261
+ direction === 'followers' ? user._count?.followers : user._count?.following;
247
262
  const profileTotal = typeof rawCount === 'number' ? rawCount : undefined;
248
263
 
249
264
  if (!page) {
@@ -256,7 +271,9 @@ export function createActorRouter(config: ActorRouterConfig): Router {
256
271
  totalItems = (await config.fetchFollowPage(userId, direction, 0, 1)).total;
257
272
  } catch (err) {
258
273
  logger.warn('[Federation] follow-collection summary total lookup failed', {
259
- username, direction, error: err,
274
+ username,
275
+ direction,
276
+ error: err,
260
277
  });
261
278
  }
262
279
  }
@@ -277,7 +294,12 @@ export function createActorRouter(config: ActorRouterConfig): Router {
277
294
  let total = profileTotal ?? 0;
278
295
  let hasMore = false;
279
296
  try {
280
- const pageResult = await config.fetchFollowPage(userId, direction, offset, FOLLOW_PAGE_SIZE);
297
+ const pageResult = await config.fetchFollowPage(
298
+ userId,
299
+ direction,
300
+ offset,
301
+ FOLLOW_PAGE_SIZE,
302
+ );
281
303
  members = pageResult.members;
282
304
  total = pageResult.total;
283
305
  hasMore = pageResult.hasMore;
@@ -285,7 +307,10 @@ export function createActorRouter(config: ActorRouterConfig): Router {
285
307
  // Fail-soft: never 500 the whole collection on an Oxy graph hiccup — serve
286
308
  // an empty page against the best-known total rather than crashing.
287
309
  logger.warn('[Federation] follow-collection Oxy graph list failed, serving empty page', {
288
- username, direction, offset, error: err,
310
+ username,
311
+ direction,
312
+ offset,
313
+ error: err,
289
314
  });
290
315
  }
291
316
 
@@ -293,9 +318,10 @@ export function createActorRouter(config: ActorRouterConfig): Router {
293
318
  .map((member) => memberActorUri(member, urls))
294
319
  .filter((uri): uri is string => uri !== null);
295
320
 
296
- const pageId = offset > 0
297
- ? `${collectionUrl(username)}?page=true&offset=${offset}`
298
- : `${collectionUrl(username)}?page=true`;
321
+ const pageId =
322
+ offset > 0
323
+ ? `${collectionUrl(username)}?page=true&offset=${offset}`
324
+ : `${collectionUrl(username)}?page=true`;
299
325
 
300
326
  const pageResponse: Record<string, unknown> = {
301
327
  '@context': AP_CONTEXT,
@@ -81,7 +81,10 @@ export interface DeliverSingleHopInit {
81
81
  * response WITHOUT following redirects. Mention adapts its `@oxy.so/core/server`-
82
82
  * backed `fetchUpstreamSingleHop` into this shape.
83
83
  */
84
- export type DeliverSingleHop = (url: string, init: DeliverSingleHopInit) => Promise<DeliverSingleHopResult>;
84
+ export type DeliverSingleHop = (
85
+ url: string,
86
+ init: DeliverSingleHopInit,
87
+ ) => Promise<DeliverSingleHopResult>;
85
88
 
86
89
  /** A durable-delivery job body (BullMQ + the fallback queue share this shape). */
87
90
  export interface DeliveryQueueJob {
@@ -94,9 +97,8 @@ export interface DeliveryQueueJob {
94
97
  * The durable-delivery fallback (written when BullMQ is unavailable).
95
98
  *
96
99
  * App-supplied, like every other store in this package: the engine never names
97
- * a database. Mention backs it with Postgres; the method names below are the
98
- * ones its original Mongoose collection exposed and are kept only so the
99
- * adapter shape stays stable for consumers.
100
+ * a database. Mention backs it with Postgres; the method names below are kept
101
+ * stable so the adapter shape stays stable for consumers.
100
102
  */
101
103
  export interface DeliveryFallbackQueue {
102
104
  /** Insert one fallback delivery row (`queueDelivery`). */
@@ -134,7 +136,9 @@ export interface DeliveryActorStore<TActor extends DeliveryActorFields> {
134
136
  /** One cached actor by uri (`resolveActorInbox` / `sendFollow` / `sendUndoFollow` / `sendAccept`). */
135
137
  findActorByUri(uri: string): Promise<TActor | null>;
136
138
  /** Inbox fields for many actor uris (`deliverToFollowers`, step 2). */
137
- findActorInboxesByUris(uris: string[]): Promise<Array<Pick<DeliveryActorFields, 'sharedInboxUrl' | 'inboxUrl'>>>;
139
+ findActorInboxesByUris(
140
+ uris: string[],
141
+ ): Promise<Array<Pick<DeliveryActorFields, 'sharedInboxUrl' | 'inboxUrl'>>>;
138
142
  }
139
143
 
140
144
  /** Bring-your-own-store: the AP follow records stay in the app DB behind this adapter. */
@@ -142,9 +146,16 @@ export interface DeliveryFollowStore {
142
146
  /** Accepted inbound followers' remote actor uris (`deliverToFollowers`, step 1). */
143
147
  listAcceptedInboundFollowerActorUris(localOxyUserId: string): Promise<string[]>;
144
148
  /** Upsert an outbound pending follow with its activity id (`sendFollow`). */
145
- upsertOutboundPending(localOxyUserId: string, remoteActorUri: string, activityId: string): Promise<void>;
149
+ upsertOutboundPending(
150
+ localOxyUserId: string,
151
+ remoteActorUri: string,
152
+ activityId: string,
153
+ ): Promise<void>;
146
154
  /** The outbound follow row for `(localOxyUserId, remoteActorUri)` (`sendUndoFollow`). */
147
- findOutbound(localOxyUserId: string, remoteActorUri: string): Promise<{ _id: unknown; activityId?: string } | null>;
155
+ findOutbound(
156
+ localOxyUserId: string,
157
+ remoteActorUri: string,
158
+ ): Promise<{ _id: unknown; activityId?: string } | null>;
148
159
  /** Delete a follow row by id (`sendUndoFollow`). */
149
160
  deleteById(id: unknown): Promise<void>;
150
161
  }
@@ -356,7 +367,9 @@ export function createDeliveryService<TActor extends DeliveryActorFields>(
356
367
  ...sigHeaders,
357
368
  };
358
369
 
359
- logger.debug(`[FedDeliver] POST ${targetInbox} body=${body} sig-headers=${sigHeaders.Signature?.match(/headers="([^"]+)"/)?.[1]}`);
370
+ logger.debug(
371
+ `[FedDeliver] POST ${targetInbox} body=${body} sig-headers=${sigHeaders.Signature?.match(/headers="([^"]+)"/)?.[1]}`,
372
+ );
360
373
 
361
374
  const { response, status } = await config.deliverSingleHop(targetInbox, {
362
375
  method: 'POST',
@@ -372,7 +385,9 @@ export function createDeliveryService<TActor extends DeliveryActorFields>(
372
385
  }
373
386
 
374
387
  const responseBody = await readResponsePreview(response);
375
- logger.debug(`Activity delivery failed to ${targetInbox}: ${status} body=${responseBody.slice(0, 500)}`);
388
+ logger.debug(
389
+ `Activity delivery failed to ${targetInbox}: ${status} body=${responseBody.slice(0, 500)}`,
390
+ );
376
391
  return false;
377
392
  } catch (err) {
378
393
  logger.debug(`Activity delivery error to ${targetInbox}:`, err);
@@ -402,7 +417,9 @@ export function createDeliveryService<TActor extends DeliveryActorFields>(
402
417
  .enqueueDelivery({ activityJson: activity, targetInbox, senderOxyUserId })
403
418
  .catch((err) => {
404
419
  const message = err instanceof Error ? err.message : String(err);
405
- logger.warn(`[FedDeliver] enqueue failed for ${targetInbox}, falling back to the durable queue: ${message}`);
420
+ logger.warn(
421
+ `[FedDeliver] enqueue failed for ${targetInbox}, falling back to the durable queue: ${message}`,
422
+ );
406
423
  return false;
407
424
  });
408
425
 
@@ -473,12 +490,19 @@ export function createDeliveryService<TActor extends DeliveryActorFields>(
473
490
  .enqueueDelivery({ activityJson: activity, targetInbox: inbox, senderOxyUserId })
474
491
  .catch((err) => {
475
492
  const message = err instanceof Error ? err.message : String(err);
476
- logger.warn(`[FedDeliver] follower enqueue failed for ${inbox}, falling back to the durable queue: ${message}`);
493
+ logger.warn(
494
+ `[FedDeliver] follower enqueue failed for ${inbox}, falling back to the durable queue: ${message}`,
495
+ );
477
496
  return false;
478
497
  });
479
498
 
480
499
  if (!enqueued) {
481
- durableFallback.push({ activityJson: activity, targetInbox: inbox, senderOxyUserId, nextAttemptAt: now });
500
+ durableFallback.push({
501
+ activityJson: activity,
502
+ targetInbox: inbox,
503
+ senderOxyUserId,
504
+ nextAttemptAt: now,
505
+ });
482
506
  }
483
507
  }
484
508
 
@@ -512,7 +536,9 @@ export function createDeliveryService<TActor extends DeliveryActorFields>(
512
536
  }
513
537
  } catch (err) {
514
538
  const message = err instanceof Error ? err.message : String(err);
515
- logger.warn(`[FedSync] deferred follow delivery setup failed for ${remoteActorUri}: ${message}`);
539
+ logger.warn(
540
+ `[FedSync] deferred follow delivery setup failed for ${remoteActorUri}: ${message}`,
541
+ );
516
542
  }
517
543
  })();
518
544
  }
@@ -541,9 +567,7 @@ export function createDeliveryService<TActor extends DeliveryActorFields>(
541
567
  const localActorUri = urls.actor(localUsername);
542
568
  // Use the actor _id when known, otherwise a stable hash of the URI so the
543
569
  // activity ID is deterministic across retries before the actor is cached.
544
- const activityIdSuffix = cached?._id
545
- ? String(cached._id)
546
- : encodeURIComponent(canonicalUri);
570
+ const activityIdSuffix = cached?._id ? String(cached._id) : encodeURIComponent(canonicalUri);
547
571
  const activityId = `${localActorUri}/follows/${activityIdSuffix}`;
548
572
 
549
573
  // Create or update the follow record
@@ -567,7 +591,9 @@ export function createDeliveryService<TActor extends DeliveryActorFields>(
567
591
  })
568
592
  .catch((err) => {
569
593
  const message = err instanceof Error ? err.message : String(err);
570
- logger.warn(`[FedSync] background follow delivery failed for ${canonicalUri}: ${message}`);
594
+ logger.warn(
595
+ `[FedSync] background follow delivery failed for ${canonicalUri}: ${message}`,
596
+ );
571
597
  });
572
598
  } else {
573
599
  // No cached inbox yet — resolve the actor's inbox in the background and
@@ -623,10 +649,14 @@ export function createDeliveryService<TActor extends DeliveryActorFields>(
623
649
  })
624
650
  .catch((err) => {
625
651
  const message = err instanceof Error ? err.message : String(err);
626
- logger.warn(`[FedSync] background undo-follow delivery failed for ${remoteActorUri}: ${message}`);
652
+ logger.warn(
653
+ `[FedSync] background undo-follow delivery failed for ${remoteActorUri}: ${message}`,
654
+ );
627
655
  });
628
656
  } else if (targetInbox) {
629
- logger.warn(`[FedDeliver] skipping Undo(Follow) delivery to blocked origin ${remoteActorUri}`);
657
+ logger.warn(
658
+ `[FedDeliver] skipping Undo(Follow) delivery to blocked origin ${remoteActorUri}`,
659
+ );
630
660
  }
631
661
 
632
662
  return true;
@@ -649,7 +679,9 @@ export function createDeliveryService<TActor extends DeliveryActorFields>(
649
679
  return;
650
680
  }
651
681
  if (isBlockedUrl(targetInbox) || isBlockedUrl(remoteActorUri)) {
652
- logger.warn(`[FedDeliver] refusing Accept(Follow) delivery to blocked origin ${remoteActorUri}`);
682
+ logger.warn(
683
+ `[FedDeliver] refusing Accept(Follow) delivery to blocked origin ${remoteActorUri}`,
684
+ );
653
685
  return;
654
686
  }
655
687
 
@@ -681,7 +713,9 @@ export function createDeliveryService<TActor extends DeliveryActorFields>(
681
713
  try {
682
714
  const user = await config.identity.resolveUserByUsername(username);
683
715
  if (!user) {
684
- logger.warn(`[FedDeliver] cannot federate actor update for ${username}: user not resolvable`);
716
+ logger.warn(
717
+ `[FedDeliver] cannot federate actor update for ${username}: user not resolvable`,
718
+ );
685
719
  return;
686
720
  }
687
721