From 064add6c61e896c8575d7934374b90d320e3b085 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sun, 3 May 2026 00:17:09 +0900 Subject: [PATCH 1/5] Support fixed-path actor dispatchers Added `mapActorAlias()` method to `ActorCallbackSetters` interface to support mapping fixed paths to sentinel identifiers for the actor dispatcher. This enables exposing a single, instance-level actor at a fixed path (like `/actor` for a relay or `/bot` for a bot) without leaking the sentinel identifier into the actor's URI. When an alias is mapped, the router will use it for resolving inbound requests and `Context.getActorUri()` will use it for constructing outbound actor URIs. WebFinger responses for the actor will also use the aliased URI. Fixes https://github.com/fedify-dev/fedify/issues/752 Assisted-by: Gemini CLI:gemini-3.1-pro-preview Assisted-by: Gemini CLI:gemini-3-flash-preview Assisted-by: Codex:gpt-5.5 --- CHANGES.md | 8 +++ docs/manual/actor.md | 43 +++++++++++++++ .../fedify/src/federation/builder.test.ts | 14 ++++- packages/fedify/src/federation/builder.ts | 10 ++++ packages/fedify/src/federation/federation.ts | 14 +++++ .../fedify/src/federation/middleware.test.ts | 52 +++++++++++++++++++ packages/fedify/src/federation/middleware.ts | 29 +++++++---- 7 files changed, 160 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a101698bc..c12781771 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,12 @@ To be released. ### @fedify/fedify + - Added `mapActorAlias()` method to `ActorCallbackSetters` interface to + support fixed-path actor dispatchers. This is useful for exposing a + single, instance-level actor at a fixed path, such as `/actor` for a relay + or `/bot` for a bot, without leaking a sentinel identifier into the actor's + URI. [[#752], [#753]] + - Added optional `MessageQueue.getDepth()` support, using the new `MessageQueueDepth` return type, for reporting queue backlog depth. `InProcessMessageQueue` can now report queued messages, including ready @@ -18,6 +24,8 @@ To be released. [#735]: https://github.com/fedify-dev/fedify/issues/735 [#748]: https://github.com/fedify-dev/fedify/pull/748 +[#752]: https://github.com/fedify-dev/fedify/issues/752 +[#753]: https://github.com/fedify-dev/fedify/pull/753 ### @fedify/amqp diff --git a/docs/manual/actor.md b/docs/manual/actor.md index c694a3ddd..b5e7bf37a 100644 --- a/docs/manual/actor.md +++ b/docs/manual/actor.md @@ -456,6 +456,49 @@ ctx.getActorUri("2bd304f9-36b3-44f0-bf0b-29124aafcbb4") > the argument is a valid identifier before calling the method. +Fixed-path actor URIs +--------------------- + +*This API is available since Fedify 2.3.0.* + +In some cases, you may want to expose a single, instance-level actor at a fixed +path, such as `/actor` for a relay or `/bot` for a bot, without leaking a +sentinel identifier like `__instance__` into the actor's URI. + +You can alias a fixed path to a sentinel identifier by calling +the `~ActorCallbackSetters.mapActorAlias()` method: + +~~~~ typescript +// @noErrors: 2345 2391 +import { type Federation } from "@fedify/fedify"; +import { Person } from "@fedify/vocab"; +const federation = null as unknown as Federation; +// ---cut-before--- +federation + .setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { + if (identifier === "bot") { + return new Person({ + id: ctx.getActorUri(identifier), + preferredUsername: "bot", + // ... + }); + } + // ... + }) + .mapActorAlias("/bot", "bot"); +~~~~ + +Once the alias is registered, `Context.getActorUri("bot")` will return +`https://example.com/bot` rather than `https://example.com/users/bot`. +Incoming requests to `/bot` will also correctly resolve the identifier to +`"bot"` and trigger the actor dispatcher. WebFinger responses for the actor +will also use the fixed path for the `self` link and the aliases. + +> [!TIP] +> You can map multiple fixed paths to different sentinel identifiers by calling +> the `~ActorCallbackSetters.mapActorAlias()` method multiple times. + + Decoupling actor URIs from WebFinger usernames ---------------------------------------------- diff --git a/packages/fedify/src/federation/builder.test.ts b/packages/fedify/src/federation/builder.test.ts index b5d8a1a99..b4fc1da82 100644 --- a/packages/fedify/src/federation/builder.test.ts +++ b/packages/fedify/src/federation/builder.test.ts @@ -25,7 +25,18 @@ test("FederationBuilder", async (t) => { const actorDispatcher: ActorDispatcher = (_ctx, _identifier) => { return null; }; - builder.setActorDispatcher("/users/{identifier}", actorDispatcher); + assertThrows( + () => + createFederationBuilder().setActorDispatcher( + "/users/{identifier}", + actorDispatcher, + ) + .mapActorAlias("/actor/{id}", "instance"), + RouterError, + "Path for actor alias must have no variables.", + ); + builder.setActorDispatcher("/users/{identifier}", actorDispatcher) + .mapActorAlias("/actor", "instance"); const inboxListener: InboxListener = ( _ctx, @@ -83,6 +94,7 @@ test("FederationBuilder", async (t) => { "webfinger", ); assertEquals(impl.router.route("/users/test123")?.name, "actor"); + assertEquals(impl.router.route("/actor")?.name, "actorAlias:instance"); assertEquals(impl.router.route("/users/test123/inbox")?.name, "inbox"); assertEquals(impl.router.route("/users/test123/outbox")?.name, "outbox"); assertEquals( diff --git a/packages/fedify/src/federation/builder.ts b/packages/fedify/src/federation/builder.ts index cb1253604..1e8db2df9 100644 --- a/packages/fedify/src/federation/builder.ts +++ b/packages/fedify/src/federation/builder.ts @@ -522,6 +522,16 @@ export class FederationBuilderImpl callbacks.aliasMapper = mapper; return setters; }, + mapActorAlias: (path: string, identifier: string) => { + const variables = new Router().add(path, "temp"); + if (variables.size > 0) { + throw new RouterError( + "Path for actor alias must have no variables.", + ); + } + this.router.add(path, `actorAlias:${identifier}`); + return setters; + }, authorize(predicate: AuthorizePredicate) { callbacks.authorizePredicate = predicate; return setters; diff --git a/packages/fedify/src/federation/federation.ts b/packages/fedify/src/federation/federation.ts index 6dad929aa..4b9f0a770 100644 --- a/packages/fedify/src/federation/federation.ts +++ b/packages/fedify/src/federation/federation.ts @@ -1108,6 +1108,20 @@ export interface ActorCallbackSetters { mapper: ActorAliasMapper, ): ActorCallbackSetters; + /** + * Maps a fixed path to a sentinel identifier. It is useful for exposing + * a single, instance-level actor at a fixed path, such as `/actor` for + * a relay or `/bot` for a bot. + * @param path The fixed path to map to the identifier. + * @param identifier The sentinel identifier to map the path to. + * @returns The setters object so that settings can be chained. + * @since 2.3.0 + */ + mapActorAlias( + path: string, + identifier: string, + ): ActorCallbackSetters; + /** * Specifies the conditions under which requests are authorized. * @param predicate A callback that returns whether a request is authorized. diff --git a/packages/fedify/src/federation/middleware.test.ts b/packages/fedify/src/federation/middleware.test.ts index a32b804d9..f22625254 100644 --- a/packages/fedify/src/federation/middleware.test.ts +++ b/packages/fedify/src/federation/middleware.test.ts @@ -9,6 +9,7 @@ import { getTypeId, lookupObject } from "@fedify/vocab"; import { assert, assertEquals, + assertExists, assertFalse, assertInstanceOf, assertNotEquals, @@ -301,6 +302,7 @@ test({ federation .setActorDispatcher("/users/{identifier}", () => new vocab.Person({})) + .mapActorAlias("/bot", "bot") .setKeyPairsDispatcher(() => [ { privateKey: rsaPrivateKey2, @@ -317,11 +319,19 @@ test({ ctx.getActorUri("handle"), new URL("https://example.com/users/handle"), ); + assertEquals( + ctx.getActorUri("bot"), + new URL("https://example.com/bot"), + ); assertEquals(ctx.parseUri(new URL("https://example.com/")), null); assertEquals( ctx.parseUri(new URL("https://example.com/users/handle")), { type: "actor", identifier: "handle" }, ); + assertEquals( + ctx.parseUri(new URL("https://example.com/bot")), + { type: "actor", identifier: "bot" }, + ); assertEquals(ctx.parseUri(null), null); assertEquals( await ctx.getActorKeyPairs("handle"), @@ -1132,6 +1142,7 @@ test("Federation.fetch()", async (t) => { }); }, ) + .mapActorAlias("/bot", "bot") .setKeyPairsDispatcher(() => { return [ { privateKey: rsaPrivateKey2, publicKey: rsaPublicKey2.publicKey! }, @@ -1170,6 +1181,47 @@ test("Federation.fetch()", async (t) => { assertEquals(response.status, 406); }); + await t.step("GET actor alias", async () => { + const { federation, dispatches } = createTestContext(); + + const response = await federation.fetch( + new Request("https://example.com/bot", { + method: "GET", + headers: { + "Accept": "application/activity+json", + }, + }), + { contextData: undefined }, + ); + + assertEquals(dispatches, ["bot"]); + assertEquals(response.status, 200); + const body = await response.json() as Record; + assertEquals(body.id, "https://example.com/bot"); + assertEquals(body.preferredUsername, "bot"); + }); + + await t.step("WebFinger for actor alias", async () => { + const { federation } = createTestContext(); + + const response = await federation.fetch( + new Request( + "https://example.com/.well-known/webfinger?resource=acct:bot@example.com", + ), + { contextData: undefined }, + ); + + assertEquals(response.status, 200); + const body = await response.json() as Record; + assertEquals(body.subject, "acct:bot@example.com"); + const selfLink = (body.links as Record[]).find((l) => + l.rel === "self" + ); + assertExists(selfLink); + assertEquals(selfLink.href, "https://example.com/bot"); + assert((body.aliases as string[]).includes("https://example.com/bot")); + }); + await t.step("POST with application/json", async () => { const { federation, inbox } = createTestContext(); diff --git a/packages/fedify/src/federation/middleware.ts b/packages/fedify/src/federation/middleware.ts index 8a9560bc2..188f2e0d0 100644 --- a/packages/fedify/src/federation/middleware.ts +++ b/packages/fedify/src/federation/middleware.ts @@ -1436,19 +1436,22 @@ export class FederationImpl } switch (routeName) { case "actor": + case "actorAlias": { + const identifier = route.name.startsWith("actorAlias:") + ? route.name.substring("actorAlias:".length) + : route.values.identifier; context = this.#createContext(request, contextData, { - invokedFromActorDispatcher: { - identifier: route.values.identifier, - }, + invokedFromActorDispatcher: { identifier }, }); return await handleActor(request, { - identifier: route.values.identifier, + identifier, context, actorDispatcher: this.actorCallbacks?.dispatcher, authorizePredicate: this.actorCallbacks?.authorizePredicate, onUnauthorized, onNotFound, }); + } case "object": { const typeId = route.name.replace(/^object:/, ""); const callbacks = this.objectCallbacks[typeId]; @@ -1789,10 +1792,16 @@ export class ContextImpl implements Context { } getActorUri(identifier: string): URL { - const path = this.federation.router.build( - "actor", - { identifier }, + let path = this.federation.router.build( + `actorAlias:${identifier}`, + {}, ); + if (path == null) { + path = this.federation.router.build( + "actor", + { identifier }, + ); + } if (path == null) { throw new RouterError("No actor dispatcher registered."); } @@ -1938,8 +1947,10 @@ export class ContextImpl implements Context { identifier: undefined, }; } - const identifier = route.values.identifier; - if (route.name === "actor") { + const identifier = route.name.startsWith("actorAlias:") + ? route.name.substring("actorAlias:".length) + : route.values.identifier; + if (route.name === "actor" || route.name.startsWith("actorAlias:")) { return { type: "actor", identifier, From 82658ee7b5dd3434d7c670862578d5495e802f89 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sun, 3 May 2026 03:12:02 +0900 Subject: [PATCH 2/5] Harden fixed-path actor aliases and address review feedback Harden `mapActorAlias()` by adding a duplicate registration guard and using a constant for the alias prefix. Updated JSDoc to include the `RouterError` contract. Improved test coverage with a duplicate alias registration test and added a null-guard for WebFinger aliases in the middleware test. https://github.com/fedify-dev/fedify/pull/753#discussion_r3176918460 https://github.com/fedify-dev/fedify/pull/753#discussion_r3176918461 https://github.com/fedify-dev/fedify/pull/753#discussion_r3176918462 https://github.com/fedify-dev/fedify/pull/753#discussion_r3176918963 https://github.com/fedify-dev/fedify/pull/753#discussion_r3176974342 https://github.com/fedify-dev/fedify/pull/753#discussion_r3176974346 Assisted-by: Gemini CLI:gemini-3-flash-preview Assisted-by: Codex:gpt-5.5 --- packages/fedify/src/federation/builder.test.ts | 9 +++++++++ packages/fedify/src/federation/builder.ts | 9 ++++++++- packages/fedify/src/federation/federation.ts | 2 ++ packages/fedify/src/federation/middleware.test.ts | 1 + packages/fedify/src/federation/middleware.ts | 14 ++++++++------ 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/fedify/src/federation/builder.test.ts b/packages/fedify/src/federation/builder.test.ts index b4fc1da82..f7931345c 100644 --- a/packages/fedify/src/federation/builder.test.ts +++ b/packages/fedify/src/federation/builder.test.ts @@ -35,6 +35,15 @@ test("FederationBuilder", async (t) => { RouterError, "Path for actor alias must have no variables.", ); + assertThrows( + () => + createFederationBuilder() + .setActorDispatcher("/users/{identifier}", actorDispatcher) + .mapActorAlias("/actor", "instance") + .mapActorAlias("/bot", "instance"), + RouterError, + 'Actor alias for "instance" already set.', + ); builder.setActorDispatcher("/users/{identifier}", actorDispatcher) .mapActorAlias("/actor", "instance"); diff --git a/packages/fedify/src/federation/builder.ts b/packages/fedify/src/federation/builder.ts index 1e8db2df9..a5ed5cf12 100644 --- a/packages/fedify/src/federation/builder.ts +++ b/packages/fedify/src/federation/builder.ts @@ -63,6 +63,8 @@ import type { } from "./handler.ts"; import { Router, RouterError } from "./router.ts"; +const ACTOR_ALIAS_PREFIX = "actorAlias:"; + function validateSingleIdentifierVariablePath( path: string, errorMessage: string, @@ -523,13 +525,18 @@ export class FederationBuilderImpl return setters; }, mapActorAlias: (path: string, identifier: string) => { + if (this.router.has(`${ACTOR_ALIAS_PREFIX}${identifier}`)) { + throw new RouterError( + `Actor alias for "${identifier}" already set.`, + ); + } const variables = new Router().add(path, "temp"); if (variables.size > 0) { throw new RouterError( "Path for actor alias must have no variables.", ); } - this.router.add(path, `actorAlias:${identifier}`); + this.router.add(path, `${ACTOR_ALIAS_PREFIX}${identifier}`); return setters; }, authorize(predicate: AuthorizePredicate) { diff --git a/packages/fedify/src/federation/federation.ts b/packages/fedify/src/federation/federation.ts index 4b9f0a770..a8985a906 100644 --- a/packages/fedify/src/federation/federation.ts +++ b/packages/fedify/src/federation/federation.ts @@ -1115,6 +1115,8 @@ export interface ActorCallbackSetters { * @param path The fixed path to map to the identifier. * @param identifier The sentinel identifier to map the path to. * @returns The setters object so that settings can be chained. + * @throws {RouterError} If the provided path or identifier is invalid or fails + * runtime validation. * @since 2.3.0 */ mapActorAlias( diff --git a/packages/fedify/src/federation/middleware.test.ts b/packages/fedify/src/federation/middleware.test.ts index f22625254..da3845e88 100644 --- a/packages/fedify/src/federation/middleware.test.ts +++ b/packages/fedify/src/federation/middleware.test.ts @@ -1219,6 +1219,7 @@ test("Federation.fetch()", async (t) => { ); assertExists(selfLink); assertEquals(selfLink.href, "https://example.com/bot"); + assertExists(body.aliases); assert((body.aliases as string[]).includes("https://example.com/bot")); }); diff --git a/packages/fedify/src/federation/middleware.ts b/packages/fedify/src/federation/middleware.ts index 188f2e0d0..6fa824155 100644 --- a/packages/fedify/src/federation/middleware.ts +++ b/packages/fedify/src/federation/middleware.ts @@ -112,6 +112,8 @@ import type { } from "./queue.ts"; import { createExponentialBackoffPolicy, type RetryPolicy } from "./retry.ts"; import { RouterError } from "./router.ts"; + +const ACTOR_ALIAS_PREFIX = "actorAlias:"; import { extractInboxes, sendActivity, @@ -1437,8 +1439,8 @@ export class FederationImpl switch (routeName) { case "actor": case "actorAlias": { - const identifier = route.name.startsWith("actorAlias:") - ? route.name.substring("actorAlias:".length) + const identifier = route.name.startsWith(ACTOR_ALIAS_PREFIX) + ? route.name.substring(ACTOR_ALIAS_PREFIX.length) : route.values.identifier; context = this.#createContext(request, contextData, { invokedFromActorDispatcher: { identifier }, @@ -1793,7 +1795,7 @@ export class ContextImpl implements Context { getActorUri(identifier: string): URL { let path = this.federation.router.build( - `actorAlias:${identifier}`, + `${ACTOR_ALIAS_PREFIX}${identifier}`, {}, ); if (path == null) { @@ -1947,10 +1949,10 @@ export class ContextImpl implements Context { identifier: undefined, }; } - const identifier = route.name.startsWith("actorAlias:") - ? route.name.substring("actorAlias:".length) + const identifier = route.name.startsWith(ACTOR_ALIAS_PREFIX) + ? route.name.substring(ACTOR_ALIAS_PREFIX.length) : route.values.identifier; - if (route.name === "actor" || route.name.startsWith("actorAlias:")) { + if (route.name === "actor" || route.name.startsWith(ACTOR_ALIAS_PREFIX)) { return { type: "actor", identifier, From 80322f006b0e6b972a163c9f3c6c0908cd3d9696 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sun, 3 May 2026 06:41:41 +0900 Subject: [PATCH 3/5] Add path-validation test to middleware.test.ts Verified that `mapActorAlias()` rejects paths containing route variables in `middleware.test.ts`, matching the behavior tested in `builder.test.ts`. Assisted-by: Gemini CLI:gemini-3-flash-preview Assisted-by: Codex:gpt-5.5 --- packages/fedify/src/federation/middleware.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/fedify/src/federation/middleware.test.ts b/packages/fedify/src/federation/middleware.test.ts index da3845e88..d79e9ab18 100644 --- a/packages/fedify/src/federation/middleware.test.ts +++ b/packages/fedify/src/federation/middleware.test.ts @@ -300,6 +300,16 @@ test({ new URL("https://example.com/nodeinfo/2.1"), ); + assertThrows( + () => + createFederation({ + kv: new MemoryKvStore(), + }).setActorDispatcher("/users/{identifier}", () => null) + .mapActorAlias("/actor/{id}" as `/${string}`, "instance"), + RouterError, + "Path for actor alias must have no variables.", + ); + federation .setActorDispatcher("/users/{identifier}", () => new vocab.Person({})) .mapActorAlias("/bot", "bot") From 2334b6d9be987646a69673e8b9b04cfbc3f50b6a Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Mon, 4 May 2026 05:49:52 +0900 Subject: [PATCH 4/5] Refactor actor alias logic and harden type safety Refactored `getActorUri()` to use a more idiomatic `??` pattern and avoid `let`. Hardened the `mapActorAlias()` signature by using a template literal type for the `path` parameter. Consolidated the `ACTOR_ALIAS_PREFIX` constant by exporting it from the builder and reusing it in the middleware. https://github.com/fedify-dev/fedify/pull/753#discussion_r3178412550 https://github.com/fedify-dev/fedify/pull/753#discussion_r3178494279 https://github.com/fedify-dev/fedify/pull/753#discussion_r3178507819 Assisted-by: Gemini CLI:gemini-3-flash-preview Assisted-by: Codex:gpt-5.5 --- packages/fedify/src/federation/builder.test.ts | 9 +++++++++ packages/fedify/src/federation/builder.ts | 10 ++++++++-- packages/fedify/src/federation/federation.ts | 2 +- packages/fedify/src/federation/middleware.test.ts | 2 ++ packages/fedify/src/federation/middleware.ts | 13 +++++-------- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/fedify/src/federation/builder.test.ts b/packages/fedify/src/federation/builder.test.ts index f7931345c..487f92194 100644 --- a/packages/fedify/src/federation/builder.test.ts +++ b/packages/fedify/src/federation/builder.test.ts @@ -44,6 +44,15 @@ test("FederationBuilder", async (t) => { RouterError, 'Actor alias for "instance" already set.', ); + assertThrows( + () => + createFederationBuilder() + .setActorDispatcher("/users/{identifier}", actorDispatcher) + .mapActorAlias("/actor", "instance") + .mapActorAlias("/actor", "bot"), + RouterError, + 'Actor alias path "/actor" conflicts with existing route "actorAlias:instance".', + ); builder.setActorDispatcher("/users/{identifier}", actorDispatcher) .mapActorAlias("/actor", "instance"); diff --git a/packages/fedify/src/federation/builder.ts b/packages/fedify/src/federation/builder.ts index a5ed5cf12..73ca4f61e 100644 --- a/packages/fedify/src/federation/builder.ts +++ b/packages/fedify/src/federation/builder.ts @@ -63,7 +63,7 @@ import type { } from "./handler.ts"; import { Router, RouterError } from "./router.ts"; -const ACTOR_ALIAS_PREFIX = "actorAlias:"; +export const ACTOR_ALIAS_PREFIX = "actorAlias:"; function validateSingleIdentifierVariablePath( path: string, @@ -524,7 +524,7 @@ export class FederationBuilderImpl callbacks.aliasMapper = mapper; return setters; }, - mapActorAlias: (path: string, identifier: string) => { + mapActorAlias: (path: `/${string}`, identifier: string) => { if (this.router.has(`${ACTOR_ALIAS_PREFIX}${identifier}`)) { throw new RouterError( `Actor alias for "${identifier}" already set.`, @@ -536,6 +536,12 @@ export class FederationBuilderImpl "Path for actor alias must have no variables.", ); } + const existingRoute = this.router.route(path); + if (existingRoute != null) { + throw new RouterError( + `Actor alias path "${path}" conflicts with existing route "${existingRoute.name}".`, + ); + } this.router.add(path, `${ACTOR_ALIAS_PREFIX}${identifier}`); return setters; }, diff --git a/packages/fedify/src/federation/federation.ts b/packages/fedify/src/federation/federation.ts index a8985a906..fa91f2253 100644 --- a/packages/fedify/src/federation/federation.ts +++ b/packages/fedify/src/federation/federation.ts @@ -1120,7 +1120,7 @@ export interface ActorCallbackSetters { * @since 2.3.0 */ mapActorAlias( - path: string, + path: `/${string}`, identifier: string, ): ActorCallbackSetters; diff --git a/packages/fedify/src/federation/middleware.test.ts b/packages/fedify/src/federation/middleware.test.ts index d79e9ab18..9390a1bf8 100644 --- a/packages/fedify/src/federation/middleware.test.ts +++ b/packages/fedify/src/federation/middleware.test.ts @@ -1224,6 +1224,8 @@ test("Federation.fetch()", async (t) => { assertEquals(response.status, 200); const body = await response.json() as Record; assertEquals(body.subject, "acct:bot@example.com"); + assertExists(body.links); + assert(Array.isArray(body.links)); const selfLink = (body.links as Record[]).find((l) => l.rel === "self" ); diff --git a/packages/fedify/src/federation/middleware.ts b/packages/fedify/src/federation/middleware.ts index 6fa824155..27b8a6dcc 100644 --- a/packages/fedify/src/federation/middleware.ts +++ b/packages/fedify/src/federation/middleware.ts @@ -112,8 +112,8 @@ import type { } from "./queue.ts"; import { createExponentialBackoffPolicy, type RetryPolicy } from "./retry.ts"; import { RouterError } from "./router.ts"; +import { ACTOR_ALIAS_PREFIX } from "./builder.ts"; -const ACTOR_ALIAS_PREFIX = "actorAlias:"; import { extractInboxes, sendActivity, @@ -1794,16 +1794,13 @@ export class ContextImpl implements Context { } getActorUri(identifier: string): URL { - let path = this.federation.router.build( + const path = this.federation.router.build( `${ACTOR_ALIAS_PREFIX}${identifier}`, {}, + ) ?? this.federation.router.build( + "actor", + { identifier }, ); - if (path == null) { - path = this.federation.router.build( - "actor", - { identifier }, - ); - } if (path == null) { throw new RouterError("No actor dispatcher registered."); } From 44b1711f5a69f847871513814243d647e1cb0316 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Mon, 4 May 2026 10:51:59 +0900 Subject: [PATCH 5/5] Add validation for empty identifier in mapActorAlias Ensured that the `identifier` parameter in `mapActorAlias()` is not an empty string. Added a regression test to verify this validation. https://github.com/fedify-dev/fedify/pull/753#discussion_r3178982269 Assisted-by: Gemini CLI:gemini-3-flash-preview Assisted-by: Codex:gpt-5.5 --- packages/fedify/src/federation/builder.test.ts | 8 ++++++++ packages/fedify/src/federation/builder.ts | 3 +++ 2 files changed, 11 insertions(+) diff --git a/packages/fedify/src/federation/builder.test.ts b/packages/fedify/src/federation/builder.test.ts index 487f92194..9d691c40d 100644 --- a/packages/fedify/src/federation/builder.test.ts +++ b/packages/fedify/src/federation/builder.test.ts @@ -53,6 +53,14 @@ test("FederationBuilder", async (t) => { RouterError, 'Actor alias path "/actor" conflicts with existing route "actorAlias:instance".', ); + assertThrows( + () => + createFederationBuilder() + .setActorDispatcher("/users/{identifier}", actorDispatcher) + .mapActorAlias("/actor", ""), + RouterError, + "Identifier cannot be empty.", + ); builder.setActorDispatcher("/users/{identifier}", actorDispatcher) .mapActorAlias("/actor", "instance"); diff --git a/packages/fedify/src/federation/builder.ts b/packages/fedify/src/federation/builder.ts index 73ca4f61e..da5cf9845 100644 --- a/packages/fedify/src/federation/builder.ts +++ b/packages/fedify/src/federation/builder.ts @@ -525,6 +525,9 @@ export class FederationBuilderImpl return setters; }, mapActorAlias: (path: `/${string}`, identifier: string) => { + if (identifier === "") { + throw new RouterError("Identifier cannot be empty."); + } if (this.router.has(`${ACTOR_ALIAS_PREFIX}${identifier}`)) { throw new RouterError( `Actor alias for "${identifier}" already set.`,