feat(federation): add /users/by-home-id reverse lookup for stub backfill

HMAC-authenticated, rate-limited (60/min/peer) endpoint that resolves a
homeUserId on this instance to its canonical username + profile snapshot.
Native non-deleted users only. Mirrors /users/lookup's auth shape.

Adds lookupRemoteUserByHomeId to federationLookup.ts as the client-side
helper. Used by the upcoming stub-backfill worker on peers that hold legacy
snowflake-named replicas of users now visible by their real handle.
This commit is contained in:
Jannis Braun
2026-05-05 15:54:06 +02:00
parent 097eb9a2ef
commit b2faf5afaa
3 changed files with 224 additions and 0 deletions
@@ -0,0 +1,74 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { bootTwoInstances, type TwoInstanceHarness } from './helpers/twoInstanceHarness.js';
import { peerInstances } from './helpers/seedPeer.js';
import { registerLocal } from './helpers/testUsers.js';
import { buildHeadersForOrigin } from './helpers/hmacSign.js';
let harness: TwoInstanceHarness;
let sharedSecret: string;
beforeAll(async () => {
harness = await bootTwoInstances();
sharedSecret = await peerInstances(harness.home, harness.remote);
}, 90_000);
afterAll(async () => {
await harness.cleanup();
});
describe('POST /api/federation/users/by-home-id', () => {
it('returns canonical username + profile for a native user when looked up by homeUserId', async () => {
const target = await registerLocal(harness.remote, 'lookup_target');
const body = JSON.stringify({ homeUserId: target.id });
const headers = buildHeadersForOrigin(body, sharedSecret, `https://${harness.home.domain}`);
const res = await fetch(`${harness.remote.origin}/api/federation/users/by-home-id`, {
method: 'POST',
headers,
body,
});
expect(res.status).toBe(200);
const json = await res.json() as { found: boolean; user?: { homeUserId: string; username: string; profile: { displayName: string | null } } };
expect(json.found).toBe(true);
expect(json.user!.homeUserId).toBe(target.id);
expect(json.user!.username).toBe(target.username);
});
it('returns { found: false } on unknown homeUserId', async () => {
const body = JSON.stringify({ homeUserId: 'definitely-not-a-real-id' });
const headers = buildHeadersForOrigin(body, sharedSecret, `https://${harness.home.domain}`);
const res = await fetch(`${harness.remote.origin}/api/federation/users/by-home-id`, {
method: 'POST',
headers,
body,
});
expect(res.status).toBe(200);
const json = await res.json() as { found: boolean };
expect(json.found).toBe(false);
});
it('rejects unsigned requests with 401', async () => {
const res = await fetch(`${harness.remote.origin}/api/federation/users/by-home-id`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ homeUserId: 'anything' }),
});
expect(res.status).toBe(401);
});
it('rejects non-native targets (replicated stubs) with { found: false }', async () => {
// Pre-existing stub on remote whose homeInstance is non-null. Use the
// first registered native user as a sanity comparison: a homeUserId that
// doesn't match a native non-deleted row → not found.
const stubLikeBody = JSON.stringify({ homeUserId: 'no-such-stub-id' });
const headers = buildHeadersForOrigin(stubLikeBody, sharedSecret, `https://${harness.home.domain}`);
const res = await fetch(`${harness.remote.origin}/api/federation/users/by-home-id`, {
method: 'POST',
headers,
body: stubLikeBody,
});
expect(res.status).toBe(200);
const json = await res.json() as { found: boolean };
expect(json.found).toBe(false);
});
});