Loading CHANGES.md +44 −4 Original line number Diff line number Diff line Loading @@ -10,9 +10,43 @@ To be released. ### @fedify/fedify - Implemented [FEP-fe34] origin-based security model to protect against content spoofing attacks and ensure secure federation practices. The security model enforces same-origin policy for ActivityPub objects and their properties, preventing malicious actors from impersonating content from other servers. [[#440]] - Added `crossOrigin` option to Activity Vocabulary property accessors (`get*()` methods) with three security levels: `"ignore"` (default, logs warning and returns `null`), `"throw"` (throws error), and `"trust"` (bypasses checks). - Added `LookupObjectOptions.crossOrigin` option to `lookupObject()` function and `Context.lookupObject()` method for controlling cross-origin validation. - Embedded objects are now validated against their parent object's origin and only trusted when they share the same origin or are explicitly marked as trusted. - Property hydration now respects origin-based security, automatically performing remote fetches when embedded objects have different origins. - Internal trust tracking system maintains security context throughout object lifecycles (construction, cloning, and property access). - Added `withIdempotency()` method to configure activity idempotency strategies for inbox processing. This addresses issue [#441] where activities with the same ID sent to different inboxes were incorrectly deduplicated globally instead of per-inbox. [[#441]] - Added `IdempotencyStrategy` type. - Added `IdempotencyKeyCallback` type. - Added `InboxListenerSetters.withIdempotency()` method. - By default, `"per-origin"` strategy is used for backward compatibility. This will change to `"per-inbox"` in Fedify 2.0. We recommend explicitly setting the strategy to avoid unexpected behavior changes. - Fixed handling of ActivityPub objects containing relative URLs. The Activity Vocabulary classes now properly resolve relative URLs when a `baseUrl` option is provided to `fromJsonLd()` method, improving Activity Vocabulary classes now automatically resolve relative URLs by inferring the base URL from the object's `@id` or document URL, eliminating the need for manual `baseUrl` specification in most cases. This improves interoperability with ActivityPub servers that emit relative URLs in properties like `icon.url` and `image.url`. [[#411], [#443] by Jiwon Kwon] Loading Loading @@ -72,6 +106,7 @@ To be released. Node.js's `--experimental-require-module` flag and resolves dual package hazard issues. [[#429], [#431]] [FEP-fe34]: https://w3id.org/fep/fe34 [FEP-5711]: https://w3id.org/fep/5711 [OStatus 1.0 Draft 2]: https://www.w3.org/community/ostatus/wiki/images/9/93/OStatus_1.0_Draft_2.pdf [RFC 7033 Section 4.4.4.3]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3 Loading @@ -87,6 +122,8 @@ To be released. [#411]: https://github.com/fedify-dev/fedify/issues/411 [#429]: https://github.com/fedify-dev/fedify/issues/429 [#431]: https://github.com/fedify-dev/fedify/pull/431 [#440]: https://github.com/fedify-dev/fedify/issues/440 [#441]: https://github.com/fedify-dev/fedify/issues/441 [#443]: https://github.com/fedify-dev/fedify/pull/443 ### @fedify/cli Loading Loading @@ -230,7 +267,7 @@ Released on September 17, 2025. Version 1.8.10 -------------- Released on Steptember 17, 2025. Released on September 17, 2025. ### @fedify/fedify Loading Loading @@ -5197,4 +5234,7 @@ Version 0.1.0 Initial release. Released on March 8, 2024. <!-- cSpell: ignore Dogeon Fabien Wressell Emelia Fróði Karlsson --> <!-- cSpell: ignore Hana Heesun Kyunghee Jiyu Revath Kumar --> <!-- cSpell: ignore Hana Heesun Kyunghee Jiyu Revath Kumar Jaeyeol --> <!-- cSpell: ignore Jiwon Kwon Hyeonseo Chanhaeng Hasang Hyunchae KeunHyeong --> <!-- cSpell: ignore Jang Hanarae ByeongJun Subin --> <!-- cSpell: ignore Wayst Konsole Ghostty Aplc --> FEDERATION.md +2 −2 Original line number Diff line number Diff line Loading @@ -31,7 +31,7 @@ Supported FEPs - [FEP-8b32][]: Object Integrity Proofs - [FEP-521a][]: Representing actor's public keys - [FEP-5feb][]: Search indexing consent for actors - [FEP-c7d3][]: Ownership - [FEP-fe34][]: Origin-based security model - [FEP-c0e0][]: Emoji reactions - [FEP-e232][]: Object Links Loading @@ -42,7 +42,7 @@ Supported FEPs [FEP-8b32]: https://w3id.org/fep/8b32 [FEP-521a]: https://w3id.org/fep/521a [FEP-5feb]: https://w3id.org/fep/5feb [FEP-c7d3]: https://w3id.org/fep/c7d3 [FEP-fe34]: https://w3id.org/fep/fe34 [FEP-c0e0]: https://w3id.org/fep/c0e0 [FEP-e232]: https://w3id.org/fep/e232 Loading cspell.json +1 −0 Original line number Diff line number Diff line Loading @@ -53,6 +53,7 @@ "keypair", "langstr", "Lemmy", "lifecycles", "litepub", "logtape", "lume", Loading docs/manual/context.md +38 −0 Original line number Diff line number Diff line Loading @@ -412,6 +412,44 @@ const note = await ctx.lookupObject( > `DocumentLoader`*](#getting-an-authenticated-documentloader) > section for details. > [!CAUTION] > For security reasons, the `~Context.lookupObject()` method implements > origin-based validation following [FEP-fe34]. If the fetched JSON-LD > document contains an `@id` that has a different origin than the requested > URL, the method will return `null` by default to prevent content spoofing > attacks. > > For example, if you request `https://example.com/notes/123` but the fetched > document has `@id: "https://malicious.com/notes/456"`, the method will > refuse to return the object and log a warning instead. > > You can control this behavior using the `crossOrigin` option: > > ~~~~ typescript twoslash > import { type Context } from "@fedify/fedify"; > const ctx = null as unknown as Context<void>; > // ---cut-before--- > // Default behavior: return null for cross-origin objects (recommended) > const objectDefault = await ctx.lookupObject("https://example.com/notes/123"); > > // Throw an error when encountering cross-origin objects > const objectStrict = await ctx.lookupObject( > "https://example.com/notes/123", > { crossOrigin: "throw" } > ); > > // Bypass origin checks (not recommended, potential security risk) > const objectBypass = await ctx.lookupObject( > "https://example.com/notes/123", > { crossOrigin: "trust" } > ); > ~~~~ > > Only use `crossOrigin: "trust"` if you fully understand the security > implications and have implemented additional validation measures. [FEP-fe34]: https://w3id.org/fep/fe34 WebFinger lookups ----------------- Loading docs/manual/inbox.md +90 −0 Original line number Diff line number Diff line Loading @@ -384,6 +384,96 @@ duplicate retry mechanisms and leverages the backend's optimized retry features. [`@fedify/redis`]: https://github.com/fedify-dev/fedify/tree/main/packages/redis Activity idempotency -------------------- *This API is available since Fedify 1.9.0.* In ActivityPub, the same activity might be delivered multiple times to your inbox for various reasons, such as network failures, server restarts, or federation protocol retries. To prevent processing the same activity multiple times, Fedify provides idempotency mechanisms that detect and skip duplicate activities. ### Idempotency strategies Fedify supports three built-in idempotency strategies: `"per-inbox"` : Activities are deduplicated per inbox. The same activity ID can be processed once per inbox, allowing the same activity to be delivered to multiple inboxes independently. This follows standard ActivityPub behavior and will be the default in Fedify 2.0. `"per-origin"` : Activities are deduplicated per receiving server's origin. The same activity ID will be processed only once on each receiving server, but can be processed separately on different receiving servers. This was the default behavior in Fedify 1.x versions. `"global"` : Activities are deduplicated globally across all inboxes and origins. The same activity ID will be processed only once, regardless of which inbox receives it or which server sent it. You can configure the idempotency strategy using the `~InboxListenerSetters.withIdempotency()` method: ~~~~ typescript twoslash import { type Federation, Follow } from "@fedify/fedify"; const federation = null as unknown as Federation<void>; // ---cut-before--- federation .setInboxListeners("/users/{identifier}/inbox", "/inbox") .withIdempotency("per-inbox") // Standard ActivityPub behavior .on(Follow, async (ctx, follow) => { // Handle the follow activity }); ~~~~ > [!WARNING] > If you don't explicitly configure an idempotency strategy, Fedify currently > uses `"per-origin"` as the default for backward compatibility. However, this > default will change to `"per-inbox"` in Fedify 2.0. We recommend explicitly > setting the strategy to avoid unexpected behavior changes. ### Custom idempotency strategy If the built-in strategies don't meet your needs, you can implement a custom idempotency strategy by providing a callback function. The callback receives the inbox context and the activity, and should return a unique cache key for the activity, or `null` to skip idempotency checking for that activity: ~~~~ typescript twoslash import { type Federation, Follow } from "@fedify/fedify"; const federation = null as unknown as Federation<void>; // ---cut-before--- federation .setInboxListeners("/users/{identifier}/inbox", "/inbox") .withIdempotency(async (ctx, activity) => { // Skip idempotency for Follow activities if (activity instanceof Follow) return null; // Use per-inbox strategy for other activities const inboxId = ctx.recipient == null ? "shared" : `actor\n${ctx.recipient}`; return `${ctx.origin}\n${activity.id?.href}\n${inboxId}`; }) .on(Follow, async (ctx, follow) => { // This Follow activity will not be deduplicated }); ~~~~ ### Idempotency cache Processed activities are cached for 24 hours to detect duplicates. The cache uses the same [key–value store](./kv.md) that you provided to the `createFederation()` function. Cache keys are automatically namespaced to avoid conflicts with other data. Error handling -------------- Loading Loading
CHANGES.md +44 −4 Original line number Diff line number Diff line Loading @@ -10,9 +10,43 @@ To be released. ### @fedify/fedify - Implemented [FEP-fe34] origin-based security model to protect against content spoofing attacks and ensure secure federation practices. The security model enforces same-origin policy for ActivityPub objects and their properties, preventing malicious actors from impersonating content from other servers. [[#440]] - Added `crossOrigin` option to Activity Vocabulary property accessors (`get*()` methods) with three security levels: `"ignore"` (default, logs warning and returns `null`), `"throw"` (throws error), and `"trust"` (bypasses checks). - Added `LookupObjectOptions.crossOrigin` option to `lookupObject()` function and `Context.lookupObject()` method for controlling cross-origin validation. - Embedded objects are now validated against their parent object's origin and only trusted when they share the same origin or are explicitly marked as trusted. - Property hydration now respects origin-based security, automatically performing remote fetches when embedded objects have different origins. - Internal trust tracking system maintains security context throughout object lifecycles (construction, cloning, and property access). - Added `withIdempotency()` method to configure activity idempotency strategies for inbox processing. This addresses issue [#441] where activities with the same ID sent to different inboxes were incorrectly deduplicated globally instead of per-inbox. [[#441]] - Added `IdempotencyStrategy` type. - Added `IdempotencyKeyCallback` type. - Added `InboxListenerSetters.withIdempotency()` method. - By default, `"per-origin"` strategy is used for backward compatibility. This will change to `"per-inbox"` in Fedify 2.0. We recommend explicitly setting the strategy to avoid unexpected behavior changes. - Fixed handling of ActivityPub objects containing relative URLs. The Activity Vocabulary classes now properly resolve relative URLs when a `baseUrl` option is provided to `fromJsonLd()` method, improving Activity Vocabulary classes now automatically resolve relative URLs by inferring the base URL from the object's `@id` or document URL, eliminating the need for manual `baseUrl` specification in most cases. This improves interoperability with ActivityPub servers that emit relative URLs in properties like `icon.url` and `image.url`. [[#411], [#443] by Jiwon Kwon] Loading Loading @@ -72,6 +106,7 @@ To be released. Node.js's `--experimental-require-module` flag and resolves dual package hazard issues. [[#429], [#431]] [FEP-fe34]: https://w3id.org/fep/fe34 [FEP-5711]: https://w3id.org/fep/5711 [OStatus 1.0 Draft 2]: https://www.w3.org/community/ostatus/wiki/images/9/93/OStatus_1.0_Draft_2.pdf [RFC 7033 Section 4.4.4.3]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3 Loading @@ -87,6 +122,8 @@ To be released. [#411]: https://github.com/fedify-dev/fedify/issues/411 [#429]: https://github.com/fedify-dev/fedify/issues/429 [#431]: https://github.com/fedify-dev/fedify/pull/431 [#440]: https://github.com/fedify-dev/fedify/issues/440 [#441]: https://github.com/fedify-dev/fedify/issues/441 [#443]: https://github.com/fedify-dev/fedify/pull/443 ### @fedify/cli Loading Loading @@ -230,7 +267,7 @@ Released on September 17, 2025. Version 1.8.10 -------------- Released on Steptember 17, 2025. Released on September 17, 2025. ### @fedify/fedify Loading Loading @@ -5197,4 +5234,7 @@ Version 0.1.0 Initial release. Released on March 8, 2024. <!-- cSpell: ignore Dogeon Fabien Wressell Emelia Fróði Karlsson --> <!-- cSpell: ignore Hana Heesun Kyunghee Jiyu Revath Kumar --> <!-- cSpell: ignore Hana Heesun Kyunghee Jiyu Revath Kumar Jaeyeol --> <!-- cSpell: ignore Jiwon Kwon Hyeonseo Chanhaeng Hasang Hyunchae KeunHyeong --> <!-- cSpell: ignore Jang Hanarae ByeongJun Subin --> <!-- cSpell: ignore Wayst Konsole Ghostty Aplc -->
FEDERATION.md +2 −2 Original line number Diff line number Diff line Loading @@ -31,7 +31,7 @@ Supported FEPs - [FEP-8b32][]: Object Integrity Proofs - [FEP-521a][]: Representing actor's public keys - [FEP-5feb][]: Search indexing consent for actors - [FEP-c7d3][]: Ownership - [FEP-fe34][]: Origin-based security model - [FEP-c0e0][]: Emoji reactions - [FEP-e232][]: Object Links Loading @@ -42,7 +42,7 @@ Supported FEPs [FEP-8b32]: https://w3id.org/fep/8b32 [FEP-521a]: https://w3id.org/fep/521a [FEP-5feb]: https://w3id.org/fep/5feb [FEP-c7d3]: https://w3id.org/fep/c7d3 [FEP-fe34]: https://w3id.org/fep/fe34 [FEP-c0e0]: https://w3id.org/fep/c0e0 [FEP-e232]: https://w3id.org/fep/e232 Loading
cspell.json +1 −0 Original line number Diff line number Diff line Loading @@ -53,6 +53,7 @@ "keypair", "langstr", "Lemmy", "lifecycles", "litepub", "logtape", "lume", Loading
docs/manual/context.md +38 −0 Original line number Diff line number Diff line Loading @@ -412,6 +412,44 @@ const note = await ctx.lookupObject( > `DocumentLoader`*](#getting-an-authenticated-documentloader) > section for details. > [!CAUTION] > For security reasons, the `~Context.lookupObject()` method implements > origin-based validation following [FEP-fe34]. If the fetched JSON-LD > document contains an `@id` that has a different origin than the requested > URL, the method will return `null` by default to prevent content spoofing > attacks. > > For example, if you request `https://example.com/notes/123` but the fetched > document has `@id: "https://malicious.com/notes/456"`, the method will > refuse to return the object and log a warning instead. > > You can control this behavior using the `crossOrigin` option: > > ~~~~ typescript twoslash > import { type Context } from "@fedify/fedify"; > const ctx = null as unknown as Context<void>; > // ---cut-before--- > // Default behavior: return null for cross-origin objects (recommended) > const objectDefault = await ctx.lookupObject("https://example.com/notes/123"); > > // Throw an error when encountering cross-origin objects > const objectStrict = await ctx.lookupObject( > "https://example.com/notes/123", > { crossOrigin: "throw" } > ); > > // Bypass origin checks (not recommended, potential security risk) > const objectBypass = await ctx.lookupObject( > "https://example.com/notes/123", > { crossOrigin: "trust" } > ); > ~~~~ > > Only use `crossOrigin: "trust"` if you fully understand the security > implications and have implemented additional validation measures. [FEP-fe34]: https://w3id.org/fep/fe34 WebFinger lookups ----------------- Loading
docs/manual/inbox.md +90 −0 Original line number Diff line number Diff line Loading @@ -384,6 +384,96 @@ duplicate retry mechanisms and leverages the backend's optimized retry features. [`@fedify/redis`]: https://github.com/fedify-dev/fedify/tree/main/packages/redis Activity idempotency -------------------- *This API is available since Fedify 1.9.0.* In ActivityPub, the same activity might be delivered multiple times to your inbox for various reasons, such as network failures, server restarts, or federation protocol retries. To prevent processing the same activity multiple times, Fedify provides idempotency mechanisms that detect and skip duplicate activities. ### Idempotency strategies Fedify supports three built-in idempotency strategies: `"per-inbox"` : Activities are deduplicated per inbox. The same activity ID can be processed once per inbox, allowing the same activity to be delivered to multiple inboxes independently. This follows standard ActivityPub behavior and will be the default in Fedify 2.0. `"per-origin"` : Activities are deduplicated per receiving server's origin. The same activity ID will be processed only once on each receiving server, but can be processed separately on different receiving servers. This was the default behavior in Fedify 1.x versions. `"global"` : Activities are deduplicated globally across all inboxes and origins. The same activity ID will be processed only once, regardless of which inbox receives it or which server sent it. You can configure the idempotency strategy using the `~InboxListenerSetters.withIdempotency()` method: ~~~~ typescript twoslash import { type Federation, Follow } from "@fedify/fedify"; const federation = null as unknown as Federation<void>; // ---cut-before--- federation .setInboxListeners("/users/{identifier}/inbox", "/inbox") .withIdempotency("per-inbox") // Standard ActivityPub behavior .on(Follow, async (ctx, follow) => { // Handle the follow activity }); ~~~~ > [!WARNING] > If you don't explicitly configure an idempotency strategy, Fedify currently > uses `"per-origin"` as the default for backward compatibility. However, this > default will change to `"per-inbox"` in Fedify 2.0. We recommend explicitly > setting the strategy to avoid unexpected behavior changes. ### Custom idempotency strategy If the built-in strategies don't meet your needs, you can implement a custom idempotency strategy by providing a callback function. The callback receives the inbox context and the activity, and should return a unique cache key for the activity, or `null` to skip idempotency checking for that activity: ~~~~ typescript twoslash import { type Federation, Follow } from "@fedify/fedify"; const federation = null as unknown as Federation<void>; // ---cut-before--- federation .setInboxListeners("/users/{identifier}/inbox", "/inbox") .withIdempotency(async (ctx, activity) => { // Skip idempotency for Follow activities if (activity instanceof Follow) return null; // Use per-inbox strategy for other activities const inboxId = ctx.recipient == null ? "shared" : `actor\n${ctx.recipient}`; return `${ctx.origin}\n${activity.id?.href}\n${inboxId}`; }) .on(Follow, async (ctx, follow) => { // This Follow activity will not be deduplicated }); ~~~~ ### Idempotency cache Processed activities are cached for 24 hours to detect duplicates. The cache uses the same [key–value store](./kv.md) that you provided to the `createFederation()` function. Cache keys are automatically namespaced to avoid conflicts with other data. Error handling -------------- Loading