Unverified Commit 7f5e8c6b authored by Jiwon Kwon's avatar Jiwon Kwon Committed by GitHub
Browse files

Merge branch 'main' into fix/426

parents d08cc461 cbc8aca7
Loading
Loading
Loading
Loading
+44 −4
Original line number Diff line number Diff line
@@ -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]

@@ -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
@@ -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
@@ -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

@@ -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 -->
+2 −2
Original line number Diff line number Diff line
@@ -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

@@ -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

+1 −0
Original line number Diff line number Diff line
@@ -53,6 +53,7 @@
    "keypair",
    "langstr",
    "Lemmy",
    "lifecycles",
    "litepub",
    "logtape",
    "lume",
+38 −0
Original line number Diff line number Diff line
@@ -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
-----------------
+90 −0
Original line number Diff line number Diff line
@@ -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