114 lines
2.9 KiB
TypeScript
114 lines
2.9 KiB
TypeScript
/**
|
|
* WebFinger Protocol Implementation
|
|
*
|
|
* WebFinger is used to discover ActivityPub actors from acct: URIs.
|
|
* See: https://www.rfc-editor.org/rfc/rfc7033
|
|
*/
|
|
|
|
export interface WebFingerResponse {
|
|
subject: string;
|
|
aliases?: string[];
|
|
links: WebFingerLink[];
|
|
}
|
|
|
|
export interface WebFingerLink {
|
|
rel: string;
|
|
type?: string;
|
|
href?: string;
|
|
template?: string;
|
|
}
|
|
|
|
/**
|
|
* Generate a WebFinger response for a local user
|
|
*/
|
|
export function generateWebFingerResponse(
|
|
handle: string,
|
|
nodeDomain: string
|
|
): WebFingerResponse {
|
|
const actorUrl = `https://${nodeDomain}/users/${handle}`;
|
|
|
|
return {
|
|
subject: `acct:${handle}@${nodeDomain}`,
|
|
aliases: [actorUrl],
|
|
links: [
|
|
{
|
|
rel: 'self',
|
|
type: 'application/activity+json',
|
|
href: actorUrl,
|
|
},
|
|
{
|
|
rel: 'http://webfinger.net/rel/profile-page',
|
|
type: 'text/html',
|
|
href: actorUrl,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parse a WebFinger resource query
|
|
* @param resource - The resource query (e.g., "acct:user@domain.com")
|
|
* @returns Object with handle and domain, or null if invalid
|
|
*/
|
|
export function parseWebFingerResource(resource: string): { handle: string; domain: string } | null {
|
|
// Handle acct: URI format
|
|
if (resource.startsWith('acct:')) {
|
|
const parts = resource.slice(5).split('@');
|
|
if (parts.length === 2) {
|
|
return { handle: parts[0], domain: parts[1] };
|
|
}
|
|
}
|
|
|
|
// Handle URL format
|
|
try {
|
|
const url = new URL(resource);
|
|
const pathParts = url.pathname.split('/');
|
|
const usersIndex = pathParts.indexOf('users');
|
|
if (usersIndex !== -1 && pathParts[usersIndex + 1]) {
|
|
return { handle: pathParts[usersIndex + 1], domain: url.host };
|
|
}
|
|
} catch {
|
|
// Not a valid URL
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Fetch WebFinger data for a remote user
|
|
*/
|
|
export async function fetchWebFinger(
|
|
handle: string,
|
|
domain: string
|
|
): Promise<WebFingerResponse | null> {
|
|
const resource = `acct:${handle}@${domain}`;
|
|
const url = `https://${domain}/.well-known/webfinger?resource=${encodeURIComponent(resource)}`;
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
'Accept': 'application/jrd+json, application/json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
return null;
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('WebFinger fetch failed:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the ActivityPub actor URL from a WebFinger response
|
|
*/
|
|
export function getActorUrlFromWebFinger(webfinger: WebFingerResponse): string | null {
|
|
const selfLink = webfinger.links.find(
|
|
(link) => link.rel === 'self' && link.type === 'application/activity+json'
|
|
);
|
|
return selfLink?.href ?? null;
|
|
}
|