Loading CHANGES.md +13 −0 Original line number Diff line number Diff line Loading @@ -28,6 +28,18 @@ To be released. - Added `attachSignature()` function. - Added `detachSignature()` function. - In inbox listeners, a received activity now can be forwarded to another server. [[#137]] - Added `InboxContext` interface. - Added `ForwardActivityOptions` interface. - The first parameter of the `InboxListener` callback type became `InboxContext` (was `Context`). - `Context.sendActivity()` method now adds Object Integrity Proofs to the activity to be sent only once. It had added Object Integrity Proofs to the activity for every recipient before. - WebFinger responses now include <http://webfinger.net/rel/avatar> links if the `Actor` object returned by the actor dispatcher has `icon`/`icons` property. Loading @@ -43,6 +55,7 @@ To be released. [Linked Data Signatures]: https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/ [#135]: https://github.com/dahlia/fedify/issues/135 [#137]: https://github.com/dahlia/fedify/issues/137 Version 0.15.1 Loading docs/manual/inbox.md +68 −0 Original line number Diff line number Diff line Loading @@ -277,6 +277,74 @@ federation > to the error handler. Forwarding activities to another server --------------------------------------- *This API is available since Fedify 1.0.0.* Sometimes, you may want to forward incoming activities to another server. For example, you may want to forward `Flag` activities to a moderation server. Or you may want to forward `Create` activities which reply to your server to your followers so that they can see the replies. The problem is that the recipients of the forwarded activities will not trust the forwarded activities unless they are signed by the original sender, not by you. You might think that you can just `~Context.sendActivity()` the received activity to the recipient in your inbox listener, but it doesn't work because the signature made by the original sender is stripped when the received activity is passed to the inbox listener, and `~Context.sendActivity()` will sign the activity with your key. To solve this problem, you can use the `~InboxContext.forwardActivity()` method in your inbox listener. It forwards the received activity without any modification, so the signature made by the original sender is preserved (if the activity is signed using by the original sender). The following shows an example of forwarding `Create` activities to followers: ~~~~ typescript twoslash import { Create, type Federation } from "@fedify/fedify"; const federation: Federation<void> = null as unknown as Federation<void>; federation.setInboxListeners("/{handle}/inbox", "/inbox") // ---cut-before--- .on(Create, async (ctx, create) => { if (create.toId == null) return; const to = ctx.parseUri(create.toId); if (to?.type !== "actor") return; const forwarder = to.handle; await ctx.forwardActivity({ handle: forwarder }, "followers"); }) ~~~~ > [!NOTE] > The `~InboxContext.forwardActivity()` method does not guarantee that the > forwarded activity is successfully delivered to the recipient, since > the original sender might neither sign the activity using [Linked Data > Signatures](./send.md#linked-data-signatures) nor [Object Integrity > Proofs](./send.md#object-integrity-proofs). In such cases, the recipient > probably won't trust the forwarded activity.[^2] > > If you don't want to forward unsigned activities, you can turn on > the `~ForwardActivityOptions.skipIfUnsigned` option in > the `~InboxContext.forwardActivity()` method: > > ~~~~ typescript twoslash > import { type InboxContext } from "@fedify/fedify"; > const ctx = null as unknown as InboxContext<void>; > // ---cut-before--- > await ctx.forwardActivity( > { handle: "alice" }, > "followers", > { skipIfUnsigned: true }, > ); > ~~~~ [^2]: Some implementations may try to verify the unsigned activity by fetching the original object from the original sender's server even if they don't trust the forwarded activity. However, it is not guaranteed that all implementations do so. Constructing inbox URIs ----------------------- Loading src/federation/callback.ts +5 −2 Original line number Diff line number Diff line Loading @@ -3,7 +3,7 @@ import type { Actor } from "../vocab/actor.ts"; import type { Activity, CryptographicKey } from "../vocab/mod.ts"; import type { Object } from "../vocab/vocab.ts"; import type { PageItems } from "./collection.ts"; import type { Context, RequestContext } from "./context.ts"; import type { Context, InboxContext, RequestContext } from "./context.ts"; import type { SenderKeyPair } from "./send.ts"; /** Loading Loading @@ -134,9 +134,11 @@ export type CollectionCursor< * * @typeParam TContextData The context data to pass to the {@link Context}. * @typeParam TActivity The type of activity to listen for. * @param context The inbox context. * @param activity The activity that was received. */ export type InboxListener<TContextData, TActivity extends Activity> = ( context: Context<TContextData>, context: InboxContext<TContextData>, activity: TActivity, ) => void | Promise<void>; Loading @@ -144,6 +146,7 @@ export type InboxListener<TContextData, TActivity extends Activity> = ( * A callback that handles errors in an inbox. * * @typeParam TContextData The context data to pass to the {@link Context}. * @param context The inbox context. */ export type InboxErrorHandler<TContextData> = ( context: Context<TContextData>, Loading src/federation/context.ts +59 −2 Original line number Diff line number Diff line Loading @@ -332,6 +332,48 @@ export interface RequestContext<TContextData> extends Context<TContextData> { getSignedKeyOwner(): Promise<Actor | null>; } /** * A context for inbox listeners. * @since 1.0.0 */ export interface InboxContext<TContextData> extends Context<TContextData> { /** * Forwards a received activity to the recipients' inboxes. The forwarded * activity will be signed in HTTP Signatures by the forwarder, but its * payload will not be modified, i.e., Linked Data Signatures and Object * Integrity Proofs will not be added. Therefore, if the activity is not * signed (i.e., it has neither Linked Data Signatures nor Object Integrity * Proofs), the recipient probably will not trust the activity. * @param forwarder The forwarder's handle or the forwarder's key pair(s). * @param recipients The recipients of the activity. * @param options Options for forwarding the activity. * @since 1.0.0 */ forwardActivity( forwarder: SenderKeyPair | SenderKeyPair[] | { handle: string }, recipients: Recipient | Recipient[], options?: ForwardActivityOptions, ): Promise<void>; /** * Forwards a received activity to the recipients' inboxes. The forwarded * activity will be signed in HTTP Signatures by the forwarder, but its * payload will not be modified, i.e., Linked Data Signatures and Object * Integrity Proofs will not be added. Therefore, if the activity is not * signed (i.e., it has neither Linked Data Signatures nor Object Integrity * Proofs), the recipient probably will not trust the activity. * @param forwarder The forwarder's handle. * @param recipients In this case, it must be `"followers"`. * @param options Options for forwarding the activity. * @since 1.0.0 */ forwardActivity( forwarder: { handle: string }, recipients: "followers", options?: ForwardActivityOptions, ): Promise<void>; } /** * A result of parsing an URI. */ Loading Loading @@ -384,8 +426,7 @@ export type ParseUriResult = | { type: "featuredTags"; handle: string }; /** * Options for {@link Context.sendActivity} method and * {@link Federation.sendActivity} method. * Options for {@link Context.sendActivity} method. */ export interface SendActivityOptions { /** Loading Loading @@ -413,6 +454,22 @@ export interface SendActivityOptions { excludeBaseUris?: URL[]; } /** * Options for {@link InboxContext.forwardActivity} method. * @since 1.0.0 */ export interface ForwardActivityOptions extends SendActivityOptions { /** * Whether to skip forwarding the activity if it is not signed, i.e., it has * neither Linked Data Signatures nor Object Integrity Proofs. * * If the activity is not signed, the recipient probably will not trust the * activity. Therefore, it is recommended to skip forwarding the activity * if it is not signed. */ skipIfUnsigned: boolean; } /** * A pair of a public key and a private key in various formats. * @since 0.10.0 Loading src/federation/handler.test.ts +28 −1 Original line number Diff line number Diff line import { assert, assertEquals, assertFalse } from "@std/assert"; import { signRequest } from "../sig/http.ts"; import { createRequestContext } from "../testing/context.ts"; import { createInboxContext, createRequestContext, } from "../testing/context.ts"; import { mockDocumentLoader } from "../testing/docloader.ts"; import { rsaPrivateKey3, Loading Loading @@ -1076,6 +1079,9 @@ test("handleInbox()", async () => { let response = await handleInbox(unsignedRequest, { handle: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, actorDispatcher: undefined, }); Loading @@ -1086,6 +1092,9 @@ test("handleInbox()", async () => { response = await handleInbox(unsignedRequest, { handle: "nobody", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, unsignedRequest); Loading @@ -1095,6 +1104,9 @@ test("handleInbox()", async () => { response = await handleInbox(unsignedRequest, { handle: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); Loading @@ -1103,6 +1115,9 @@ test("handleInbox()", async () => { response = await handleInbox(unsignedRequest, { handle: "someone", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); Loading @@ -1123,6 +1138,9 @@ test("handleInbox()", async () => { response = await handleInbox(signedRequest, { handle: null, context: signedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); Loading @@ -1131,6 +1149,9 @@ test("handleInbox()", async () => { response = await handleInbox(signedRequest, { handle: "someone", context: signedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); Loading @@ -1139,6 +1160,9 @@ test("handleInbox()", async () => { response = await handleInbox(unsignedRequest, { handle: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, skipSignatureVerification: true, }); Loading @@ -1148,6 +1172,9 @@ test("handleInbox()", async () => { response = await handleInbox(unsignedRequest, { handle: "someone", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, skipSignatureVerification: true, }); Loading Loading
CHANGES.md +13 −0 Original line number Diff line number Diff line Loading @@ -28,6 +28,18 @@ To be released. - Added `attachSignature()` function. - Added `detachSignature()` function. - In inbox listeners, a received activity now can be forwarded to another server. [[#137]] - Added `InboxContext` interface. - Added `ForwardActivityOptions` interface. - The first parameter of the `InboxListener` callback type became `InboxContext` (was `Context`). - `Context.sendActivity()` method now adds Object Integrity Proofs to the activity to be sent only once. It had added Object Integrity Proofs to the activity for every recipient before. - WebFinger responses now include <http://webfinger.net/rel/avatar> links if the `Actor` object returned by the actor dispatcher has `icon`/`icons` property. Loading @@ -43,6 +55,7 @@ To be released. [Linked Data Signatures]: https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/ [#135]: https://github.com/dahlia/fedify/issues/135 [#137]: https://github.com/dahlia/fedify/issues/137 Version 0.15.1 Loading
docs/manual/inbox.md +68 −0 Original line number Diff line number Diff line Loading @@ -277,6 +277,74 @@ federation > to the error handler. Forwarding activities to another server --------------------------------------- *This API is available since Fedify 1.0.0.* Sometimes, you may want to forward incoming activities to another server. For example, you may want to forward `Flag` activities to a moderation server. Or you may want to forward `Create` activities which reply to your server to your followers so that they can see the replies. The problem is that the recipients of the forwarded activities will not trust the forwarded activities unless they are signed by the original sender, not by you. You might think that you can just `~Context.sendActivity()` the received activity to the recipient in your inbox listener, but it doesn't work because the signature made by the original sender is stripped when the received activity is passed to the inbox listener, and `~Context.sendActivity()` will sign the activity with your key. To solve this problem, you can use the `~InboxContext.forwardActivity()` method in your inbox listener. It forwards the received activity without any modification, so the signature made by the original sender is preserved (if the activity is signed using by the original sender). The following shows an example of forwarding `Create` activities to followers: ~~~~ typescript twoslash import { Create, type Federation } from "@fedify/fedify"; const federation: Federation<void> = null as unknown as Federation<void>; federation.setInboxListeners("/{handle}/inbox", "/inbox") // ---cut-before--- .on(Create, async (ctx, create) => { if (create.toId == null) return; const to = ctx.parseUri(create.toId); if (to?.type !== "actor") return; const forwarder = to.handle; await ctx.forwardActivity({ handle: forwarder }, "followers"); }) ~~~~ > [!NOTE] > The `~InboxContext.forwardActivity()` method does not guarantee that the > forwarded activity is successfully delivered to the recipient, since > the original sender might neither sign the activity using [Linked Data > Signatures](./send.md#linked-data-signatures) nor [Object Integrity > Proofs](./send.md#object-integrity-proofs). In such cases, the recipient > probably won't trust the forwarded activity.[^2] > > If you don't want to forward unsigned activities, you can turn on > the `~ForwardActivityOptions.skipIfUnsigned` option in > the `~InboxContext.forwardActivity()` method: > > ~~~~ typescript twoslash > import { type InboxContext } from "@fedify/fedify"; > const ctx = null as unknown as InboxContext<void>; > // ---cut-before--- > await ctx.forwardActivity( > { handle: "alice" }, > "followers", > { skipIfUnsigned: true }, > ); > ~~~~ [^2]: Some implementations may try to verify the unsigned activity by fetching the original object from the original sender's server even if they don't trust the forwarded activity. However, it is not guaranteed that all implementations do so. Constructing inbox URIs ----------------------- Loading
src/federation/callback.ts +5 −2 Original line number Diff line number Diff line Loading @@ -3,7 +3,7 @@ import type { Actor } from "../vocab/actor.ts"; import type { Activity, CryptographicKey } from "../vocab/mod.ts"; import type { Object } from "../vocab/vocab.ts"; import type { PageItems } from "./collection.ts"; import type { Context, RequestContext } from "./context.ts"; import type { Context, InboxContext, RequestContext } from "./context.ts"; import type { SenderKeyPair } from "./send.ts"; /** Loading Loading @@ -134,9 +134,11 @@ export type CollectionCursor< * * @typeParam TContextData The context data to pass to the {@link Context}. * @typeParam TActivity The type of activity to listen for. * @param context The inbox context. * @param activity The activity that was received. */ export type InboxListener<TContextData, TActivity extends Activity> = ( context: Context<TContextData>, context: InboxContext<TContextData>, activity: TActivity, ) => void | Promise<void>; Loading @@ -144,6 +146,7 @@ export type InboxListener<TContextData, TActivity extends Activity> = ( * A callback that handles errors in an inbox. * * @typeParam TContextData The context data to pass to the {@link Context}. * @param context The inbox context. */ export type InboxErrorHandler<TContextData> = ( context: Context<TContextData>, Loading
src/federation/context.ts +59 −2 Original line number Diff line number Diff line Loading @@ -332,6 +332,48 @@ export interface RequestContext<TContextData> extends Context<TContextData> { getSignedKeyOwner(): Promise<Actor | null>; } /** * A context for inbox listeners. * @since 1.0.0 */ export interface InboxContext<TContextData> extends Context<TContextData> { /** * Forwards a received activity to the recipients' inboxes. The forwarded * activity will be signed in HTTP Signatures by the forwarder, but its * payload will not be modified, i.e., Linked Data Signatures and Object * Integrity Proofs will not be added. Therefore, if the activity is not * signed (i.e., it has neither Linked Data Signatures nor Object Integrity * Proofs), the recipient probably will not trust the activity. * @param forwarder The forwarder's handle or the forwarder's key pair(s). * @param recipients The recipients of the activity. * @param options Options for forwarding the activity. * @since 1.0.0 */ forwardActivity( forwarder: SenderKeyPair | SenderKeyPair[] | { handle: string }, recipients: Recipient | Recipient[], options?: ForwardActivityOptions, ): Promise<void>; /** * Forwards a received activity to the recipients' inboxes. The forwarded * activity will be signed in HTTP Signatures by the forwarder, but its * payload will not be modified, i.e., Linked Data Signatures and Object * Integrity Proofs will not be added. Therefore, if the activity is not * signed (i.e., it has neither Linked Data Signatures nor Object Integrity * Proofs), the recipient probably will not trust the activity. * @param forwarder The forwarder's handle. * @param recipients In this case, it must be `"followers"`. * @param options Options for forwarding the activity. * @since 1.0.0 */ forwardActivity( forwarder: { handle: string }, recipients: "followers", options?: ForwardActivityOptions, ): Promise<void>; } /** * A result of parsing an URI. */ Loading Loading @@ -384,8 +426,7 @@ export type ParseUriResult = | { type: "featuredTags"; handle: string }; /** * Options for {@link Context.sendActivity} method and * {@link Federation.sendActivity} method. * Options for {@link Context.sendActivity} method. */ export interface SendActivityOptions { /** Loading Loading @@ -413,6 +454,22 @@ export interface SendActivityOptions { excludeBaseUris?: URL[]; } /** * Options for {@link InboxContext.forwardActivity} method. * @since 1.0.0 */ export interface ForwardActivityOptions extends SendActivityOptions { /** * Whether to skip forwarding the activity if it is not signed, i.e., it has * neither Linked Data Signatures nor Object Integrity Proofs. * * If the activity is not signed, the recipient probably will not trust the * activity. Therefore, it is recommended to skip forwarding the activity * if it is not signed. */ skipIfUnsigned: boolean; } /** * A pair of a public key and a private key in various formats. * @since 0.10.0 Loading
src/federation/handler.test.ts +28 −1 Original line number Diff line number Diff line import { assert, assertEquals, assertFalse } from "@std/assert"; import { signRequest } from "../sig/http.ts"; import { createRequestContext } from "../testing/context.ts"; import { createInboxContext, createRequestContext, } from "../testing/context.ts"; import { mockDocumentLoader } from "../testing/docloader.ts"; import { rsaPrivateKey3, Loading Loading @@ -1076,6 +1079,9 @@ test("handleInbox()", async () => { let response = await handleInbox(unsignedRequest, { handle: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, actorDispatcher: undefined, }); Loading @@ -1086,6 +1092,9 @@ test("handleInbox()", async () => { response = await handleInbox(unsignedRequest, { handle: "nobody", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, unsignedRequest); Loading @@ -1095,6 +1104,9 @@ test("handleInbox()", async () => { response = await handleInbox(unsignedRequest, { handle: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); Loading @@ -1103,6 +1115,9 @@ test("handleInbox()", async () => { response = await handleInbox(unsignedRequest, { handle: "someone", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); Loading @@ -1123,6 +1138,9 @@ test("handleInbox()", async () => { response = await handleInbox(signedRequest, { handle: null, context: signedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); Loading @@ -1131,6 +1149,9 @@ test("handleInbox()", async () => { response = await handleInbox(signedRequest, { handle: "someone", context: signedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); Loading @@ -1139,6 +1160,9 @@ test("handleInbox()", async () => { response = await handleInbox(unsignedRequest, { handle: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, skipSignatureVerification: true, }); Loading @@ -1148,6 +1172,9 @@ test("handleInbox()", async () => { response = await handleInbox(unsignedRequest, { handle: "someone", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, skipSignatureVerification: true, }); Loading