v4: a remote account's links to its home lead to the home's pages — the settings' "Change it on <host>" button and the profile's "Account on <host>" link went to the host in the handle, which is the home's issuer, its API, not necessarily where its pages are: in development musique's button led to localhost:8000/settings, gerbeur's API, which served a stale build, instead of localhost:3000/settings. GET /api/users/:key/home?path= now redirects to a page of a remote user's home, their profile there by default, at the origin of the home's authorization_endpoint, which its metadata gives (homePageUrl in federation-service.ts), the issuer standing in while the home doesn't answer; a local user 404s, and a path that isn't one 400s. homeUrl links to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-09 21:06:14 +00:00
parent 5f2fb4479a
commit 8bcc005140
5 changed files with 90 additions and 9 deletions

View File

@@ -18,7 +18,8 @@
* blocked one they may not, and from any other only with an invite, which the
* register page carries. None may while the deployment turns sign-ins from
* other instances off (`FEDERATION.incoming`). Once here, an account works like any other, but its
* password, email, avatar and name are its home's (`assertLocalUser`).
* password, email, avatar and name are its home's (`assertLocalUser`), and
* change on its pages there ({@link homePageUrl}).
*
* @module
*/
@@ -40,7 +41,8 @@ import {
isIdentityClaims,
parseInstance,
} from "../../shared/federation.ts";
import { EMAIL_RE, USERNAME_RE } from "../../shared/validation.ts";
import { userPath } from "../../shared/paths.ts";
import { EMAIL_RE, splitHandle, USERNAME_RE } from "../../shared/validation.ts";
import { toPublicUser, type User } from "../model/user.ts";
import {
exchangeCode,
@@ -447,3 +449,35 @@ export async function receiveEvent(token: string): Promise<void> {
if (!claims) throw new APIException(400, "Invalid event");
await applyClaims(identity.user_id, issuer, claims);
}
/**
* Returns the URL of `path` on the home of `user`, an account from another
* instance, for `GET /api/users/:key/home`: their profile there by default,
* or another of its pages, such as `/settings`, where they change their
* password. A handle's host is its home's issuer, the API's origin, which
* needn't be its pages': those are where its `/authorize` page is, and the
* home's metadata says. While it doesn't answer, the issuer stands in.
*
* @throws {APIException} 400 when `path` isn't a path; 404 when `user` is
* local.
*/
export async function homePageUrl(user: User, path?: string): Promise<string> {
const identity = db.prepare(
`SELECT issuer FROM user_identities WHERE user_id = ?;`,
).get(user.id) as { issuer: string } | undefined;
if (!identity) throw new APIException(404, "This account lives here");
if (path !== undefined && (!path.startsWith("/") || path.startsWith("//"))) {
throw new APIException(400, "Not a path");
}
let pages = identity.issuer;
try {
const { origin, protocol } = new URL(
(await fetchMetadata(identity.issuer)).authorization_endpoint,
);
if (protocol === "https:" || protocol === "http:") pages = origin;
} catch (err) {
console.error(`[federation] ${identity.issuer} unreachable:`, err);
}
return `${pages}${path ?? userPath(splitHandle(user.username).name)}`;
}