Skip to content

Fedify 2.4.0

Latest

Choose a tag to compare

@github-actions github-actions released this 30 Sep 18:51
· 34 commits to main since this release
2.4.0
a000224

Released on October 1, 2026.

@fedify/fedify

  • Added onRequestFinished() to observe every inbox delivery, including rejected requests and preparation errors, with signature/proof checks, actual verification keys, the final authentication decision, and the processing outcome. The callback is awaited independently of trace sampling, and its errors do not change delivery results. [#1191, #1201]

  • Added support for FEP-ef61 portable objects, whose IDs are ap: or ap+ef61: URIs with a DID instead of a host, e.g., ap://did:key:z6Mk.../actor. A Fedify server can now act as a gateway for portable actors: it serves their actor documents, objects, collections, and hashlink media, accepts deliveries to their inboxes, and sends their activities. It can also consume the portable objects of others. The new Portable objects chapter of the manual walks through running portable actors, and describes the FEP-ef61 profile that Fedify supports, including where it deliberately differs from the FEP and what it does not implement. Applications without portable actors are unaffected, except that their dispatchers may be called for requests to the gateway endpoint, /.well-known/apgateway/, and that Context.sendActivity() may reject activities that embed others' portable objects (see below). [#288, #1151, #1198]

    • Added verifyPortableObjectProof(), which enforces the FEP-ef61 proof policy: a portable actor, activity, or object needs an FEP-8b32 Object Integrity Proof whose verificationMethod is a DID URL of the DID in its ID, and a portable actor needs a non-empty gateways list of HTTP(S) origins. A document whose ID is a compatible identifier, e.g., https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor, is verified against the DID in it. Its detailed result distinguishes documents outside the policy, unsecured collections, missing or invalid proofs, invalid gateways, unsupported verification methods, and DID mismatches. [#832, #968, #1093, #1105, #1148, #1178]

    • Added verifyPortableObject(), which verifies proofs like verifyPortableObjectProof(), and also accepts an actor's inbox, outbox, followers, following, or liked collection without a proof if a gateway that the actor lists served it, as FEP-ef61 allows. The VerifyPortableObjectOptions, VerifyPortableObjectResult, and VerifyPortableObjectFailureReason types describe it. [#836, #1084, #1093, #1105]

    • Added the Context.verifyPortableObject property, which is verifyPortableObject() with the context's loaders. Since it has the same name as the option of property accessors, passing a context as their options, e.g., await create.getObject(ctx), verifies portable objects. Inboxes use it as the default verifier of received activities, as do Context.lookupObject(), Context.traverseCollection(), and the onOutboxError callback. Added the verifyPortableObject option to VerifyObjectOptions. [#1107, #1120]

    • verifyProof() now resolves Ed25519 did:key verification methods, e.g., did:key:z6Mk...#z6Mk..., locally without fetching them, and verifyObject() accepts a proof made by a DID as authenticating an actor or attribution whose portable ID or compatible identifier has that DID. [#827, #829, #915, #926, #1093, #1105]

    • Added the Context.getPortableActorUri(), Context.getPortableObjectUri(), Context.getPortableInboxUri(), Context.getPortableOutboxUri(), Context.getPortableFollowingUri(), Context.getPortableFollowersUri(), Context.getPortableLikedUri(), Context.getPortableFeaturedUri(), Context.getPortableFeaturedTagsUri(), and Context.getPortableCollectionUri() methods, which build portable IDs from the paths of the corresponding dispatchers and a DID. Custom implementations of the Context interface need to implement them. [#835, #839, #841, #1076, #1092, #1111, #1114, #1142] They throw a TypeError for a did:key DID that is not encoded in base58-btc, as FEP-ef61 requires, or for an identifier that has a . or .. path segment. [#841, #1114, #1154, #1186]

    • Added the portable option to Context.parseUri() and the ParseUriOptions interface. With { portable: true }, the method also recognizes portable IDs and their compatible identifiers, and the result has the DID in its new authority property. [#1143, #1145]

    • Object dispatchers and the actor dispatcher now serve portable objects and actors through the gateway endpoint, e.g., GET /.well-known/apgateway/did:key:z6Mk.../notes/123, with the path after the DID. An object is served only if its ID canonically equals the requested portable ID and it has a proof made with a key of the DID. A non-public object is served only if the dispatcher has an authorization predicate, as FEP-ef61 forbids gateways to serve it to anyone but its audience. Signed tombstones are served with 410 Gone. Added the RequestContext.portableRequest property, which tells the dispatcher the requested DID and ID. [#835, #841, #1076, #1113, #1114, #1124, #1153, #1183]

    • Added the RequestContext.isSignedByAudience() method and the IsSignedByAudienceOptions interface, which check whether a request is signed by an actor in the audience of an object, e.g., in the authorization predicate of an object dispatcher. Custom implementations of the RequestContext interface need to implement the method. [#1153, #1183]

    • Collection dispatchers now serve the collections of portable actors through the gateway endpoint, e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice/outbox, if the actor is a portable actor under the requested DID whose corresponding property refers to the collection. A collection that is not paginated always has totalItems, so that consumers can tell an empty one from other objects. Added the CustomCollectionCallbackSetters.mapPortableOwner() method and the PortableCollectionOwnerMapper type, which tie a custom collection to its portable owner. [#1111, #1142]

    • Added Federatable.setHashlinkMediaDispatcher() and the HashlinkMediaRequest interface, which serve resources that portable objects refer to with SHA-256 hashlinks, e.g., GET /.well-known/apgateway/hl:zQm.... Fedify does not verify the dispatcher's response against the digest. Added hashlink_media to the values of the fedify.endpoint metric attribute. [#838, #1080]

    • Inbox listeners now accept deliveries to portable inboxes through the gateway endpoint, e.g., POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox, if the actor dispatcher returns a portable actor whose inbox is the requested inbox and whose gateways include this server. [#839, #1092]

    • Fedify now forwards an activity delivered to a portable inbox to the actor's other gateways at most once, as FEP-ef61 recommends, if the activity is authenticated by its own proof or Linked Data Signature and the KvStore supports cas(). Forwarded requests are signed with this server's RSA gateway key for the actor if the key pairs dispatcher returns one, and are sent unsigned otherwise. An activity is not forwarded back to the gateway that forwarded it if Fedify can identify that gateway by its gateway key signature on the delivery. Added the FederationOptions.portableInboxForwarding and FederationKvPrefixes.portableInboxForwarding options. [#839, #1092, #1100, #1101, #1104, #1110]

    • Context.sendActivity() and InboxContext.forwardActivity() now deliver to portable inboxes through the recipient's gateways, or else the @gateway location hints of the inbox, trying at most five of them until one accepts the activity. Previously, such a delivery failed with UrlError: Unsupported protocol: ap+ef61:. Queue workers of older Fedify versions deliver such queued activities only through the first gateway, so upgrade them before the servers that enqueue deliveries. [#1147, #1180]

    • Added the ActorCallbackSetters.mapPortableActorId() method and the PortableActorIdMapper type. For an actor that the callback maps to a portable ID, Context.getActorKeyPairs() identifies its key pairs by the actor's compatible identifier on this server, e.g., https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice#main-key, so that they serve as this server's gateway keys for the actor, which sign HTTP requests made on behalf of the actor, but never make Object Integrity Proofs or Linked Data Signatures. [#840, #1099]

    • HTTP Signature verification now accepts a gateway key of a portable actor if the actor document at the key ID has a valid proof by the actor's DID, embeds the key in its assertionMethod, or else in its publicKey, and lists the gateway in its gateways. Such a key is owned by the portable actor, so RequestContext.getSignedKeyOwner(), getKeyOwner(), and doesActorOwnKey() return or match the actor. Keys at compatible identifiers are cached apart for each purpose, for an hour at most. [#840, #1095, #1099, #1105, #1119, #1123, #1164] A key at an ap: key ID is accepted likewise if an actor document fetched through the key ID's location hints vouches for it. [#1096, #1132, #1134, #1165]

    • Inboxes accept an activity of a portable actor, or an activity with a portable ID, only if it has a valid proof made by the DID of that ID; an HTTP Signature or a Linked Data Signature does not authenticate it, and such an activity is rejected with 401 Unauthorized. [#840, #1099]

    • Inboxes independently verify each portable actor, activity, and object embedded in a compound document, e.g., the Note in a Create, against its own proof before dispatch, following Fedify's interim map-local profile, since neither FEP-8b32 nor Verifiable Credential Data Integrity defines the boundaries of embedded proofs yet. A valid outer proof does not authenticate an unsigned or invalid embedded portable object. Documents with proof sets, or that exceed the traversal limits, are rejected. A key embedded in a verified portable actor needs no proof of its own if the key's ID is the actor's ID plus a fragment. This profile may change in Fedify 3.0. [#938, #1041, #1094, #1102, #1133, #1138]

    • Context.sendActivity() gives an activity that contains portable objects at most one proof: an activity that already has one is sent as is, and a portable activity is signed only by the key whose ID is a DID URL for its DID. An activity of a portable actor has to have a portable ID or a compatible identifier of the actor's DID, and never gets a Linked Data Signature. Otherwise, sendActivity() rejects with a TypeError before anything is delivered or queued. So does a non-portable activity that embeds portable objects, including ones with compatible identifiers, when several Ed25519 keys are available, and an activity with portable objects in which any map carries a proof set, or an embedded map carries a proof but not its own @context, e.g., a received signed Follow embedded in an Accept, which Fedify inboxes would reject; refer to such an object by its ID instead. Sign portable activities with signObject() beforehand, or pass the DID's key as an explicit sender key. [#840, #1041, #1045, #1073, #1099]

    • Your portable actors and objects may have compatible identifiers as their IDs, e.g., https://example.com/.well-known/apgateway/did:key:z6Mk.../actor, for interoperability with software that cannot handle portable IDs. Fedify treats them as portable wherever it serves or sends them, and warns if such an ID is malformed or is not on the owner's first gateway, as FEP-ef61 requires. Build them with toCompatibleEf61Id() before signing the documents. [#1106, #1109, #1155, #1188]

    • The WebFinger endpoint now serves portable actors: the domain of the actor's address comes from the first gateway in its gateways, and its self link is its compatible identifier on that gateway. WebFinger resources can be portable IDs, which are passed to mapAlias(). Context.lookupObject() resolves the handles of portable actors on other servers. [#837, #1082, #1106, #1109]

    • Fedify logs a warning if an actor dispatcher returns a portable actor whose gateways are invalid, or whose collections are not the portable IDs or compatible identifiers that the corresponding Context.getPortable*Uri() methods build, instead of warning that a portable actor's ID or collections do not match the URIs that Context.getActorUri() and the like build. [#837, #1082, #1111, #1142, #1148, #1178]

    • Context.routeActivity() routes a portable activity that has no valid proof only if all of its actors have the same DID as the activity, comparing FEP-fe34 origins, where the origin of a portable ID or a compatible identifier is its DID. [#1146, #1171]

    • Outbox listeners compare the actor of a posted activity with a portable outbox owner by their canonical portable IDs, so that the actor can be referred to with @gateway location hints, another URI scheme, or as a compatible identifier on another gateway. [#1159, #1189]

  • Changed Fedify to treat documents whose IDs are FEP-ef61 compatible identifiers, such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor, as the portable objects they stand for, as FEP-ef61 requires, instead of trusting them by the web origin that served them. Previously, any server could act as https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor without a proof by Alice's DID. Some implementations, e.g., tootik, identify their portable actors this way. [#288, #1093, #1105]

    • Inboxes now reject an activity of an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof made by the actor's DID. Activities of actors that publish compatible identifiers without signing them, or whose DIDs use key types Fedify does not support, are no longer accepted.

    • verifyObject() never authenticates an attribution or actor whose ID is a compatible identifier by a key at an ordinary URL.

    • HTTP Signature verification rejects a key at a compatible identifier unless the actor document has a valid proof by the actor's DID and vouches for the key. A key at an ordinary URL that claims a portable actor as its owner or controller is no longer that actor's key, and getKeyOwner() and doesActorOwnKey() no longer resolve portable actors by web origin.

  • Changed property accessors to verify the FEP-ef61 portable objects that they dereference when a Context is passed as their options, e.g., await create.getObject(ctx), since the new Context.verifyPortableObject property has the same name as their verifyPortableObject option. This also applies to objects with ordinary HTTP(S) IDs: their references to compatible identifiers are now verified as portable objects instead of being trusted because of their origin. In inbox listeners, accessors do so even without options. [#288, #1107, #1120]

  • Fixed signObject() so that a signed object keeps verifying after it is assigned to a typed parent and the parent is serialized. signObject() now captures the secured JSON document its proof covers, and nested serialization embeds that document verbatim instead of rebuilding the child under the parent's JSON-LD context. Producing a compound document, such as a signed Note in a signed FEP-ef61 portable Create, with signObject() no longer requires assembling the JSON by hand. [#288, #1041, #1044, #1051]

    • The captured document is a snapshot: clone() does not carry it, and mutating a signed object in place does not change it. Sign a clone again when it has to be embedded as a secured child.
    • Serialization falls back to the previous behavior for objects parsed with fromJsonLd(), objects that already carried a proof, and toJsonLd() calls whose context option could hide the internal placeholder.
    • Outgoing JSON-LD compatibility normalization now leaves a nested self-contained secured document untouched while signing and sending, so it cannot rewrite bytes that the child's own proof covers. Inbound verification is unaffected.
    • Fanout delivery now reuses the document the activity was already serialized into instead of reparsing and reserializing it, which previously invalidated an embedded signed child and the outer proof that covered it.
  • Allowed object dispatchers to return Tombstone for deleted objects, as actor dispatchers already can. Fedify now serves such object URIs with 410 Gone and the serialized tombstone body, as ActivityPub recommends, after applying the authorization predicate as for other objects, so that applications no longer need a separate route in front of Fedify for deleted posts. Fedify logs a warning if the tombstone's id does not match Context.getObjectUri(). [#1112, #1117]

    • Changed the return type of ObjectDispatcher from TObject | null to TObject | Tombstone | null.
    • Added a RequestContext.getObject() overload that takes a GetObjectOptions object. By default, getObject() still returns null for a tombstone unless the tombstone is an instance of the requested class, e.g., Object or Tombstone. Pass { tombstone: "passthrough" } to receive tombstones.
    • Added the GetObjectOptions interface.
    • Changed object dispatchers registered for Tombstone or Object that return tombstones to be served with 410 Gone instead of 200 OK.
  • Changed HTTP Signature verification to verify at most the first three RFC 9421 signatures of a request, in the order of its Signature-Input header, and to ignore the rest. Each signature may make Fedify fetch the key that it names from a URL of the sender's choosing, so a single unauthenticated request with many signatures could make Fedify fetch any number of URLs. A signature counts even if it fails before its key is fetched, and a key is now looked up only once for all the signatures of a request that name it. A request whose only valid signature comes after the first three is no longer accepted; senders usually put a single signature on a request. [#1130, #1166]

    • Added the FederationOptions.maxHttpSignatures option to change the limit for the inbox and RequestContext.getSignedKey(), and the maxSignatures option of verifyRequest() and verifyRequestDetailed() for verifying requests directly. Both have to be positive integers or Infinity, which turns the limit off; other values throw a RangeError.

    • When a cached key does not verify a signature, Fedify now fetches it again for that signature only, instead of verifying every signature of the request once more without the key cache.

  • Changed cached actor public keys and remembered per-origin HTTP Message Signatures specs to expire, so a KvStore that never sees an explicit clear no longer accumulates entries for actors and origins that have stopped federating. Keys expire after 30 days and specs after 90 days by default, and both windows are configurable through the new FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl options. [#1017, #1027 by Heewon Chae]

    • Shortening a window trades storage for remote requests: an expired key has to be refetched before the next signature verification, and an expired spec has to be relearned by double-knocking on the next delivery. Refetching fails while the peer is unavailable, so a very short window makes verification depend on the peer being reachable.
    • Entries written by earlier versions of Fedify have no expiry and are left as they are; they gain one the next time they are written. See the new Clearing legacy cache entries section of the key–value store guide to clear them proactively instead of waiting.
  • Changed the built-in document loader, context loader, and authenticated document loader to time out each call after 10 seconds by default. Previously, a remote server that responded slowly or never could hold a request for as long as the runtime and the network allowed, e.g., an incoming activity whose signature key Fedify fetched, and the sender picks such key URLs. The time limit covers the whole call, including every redirect and alternate document it follows, double-knocking retries, and reading the response body. A call that times out throws a FetchError without a response, whose cause is a DOMException named "TimeoutError", so a key fetch that times out is reported as a keyFetchError by verifyRequestDetailed() and is cached like other failures to fetch a key. Custom document loaders are not affected. [#1131, #1169]

    • Added FederationOptions.documentLoaderTimeout option to change the time limit, or to turn it off with null.

    • Added the timeout option to getAuthenticatedDocumentLoader().

  • Changed the built-in document loaders to read the body of an error response before they throw a FetchError, at most 1 MiB, so that the time limit also bounds reading it. FetchError.response keeps the body byte for byte, or only the status and headers if the body is larger than that. [#1131, #1169]

  • Fixed outbound delivery circuit breaker transitions when a key–value store compares encoded values. Existing half-open states can now recover after switching to a CAS-capable store. [#1163, #1167]

  • Fixed verifyProof() so Ed25519 JCS proofs authenticate every received proof option except proofValue, including expires, domain, challenge, nonce, and extension options. It now rejects expired or malformed proof options, and callers can provide expected domain and challenge values through VerifyProofOptions to prevent cross-domain or replay use. [#968]

  • Added support for the ActivityPub Media Upload extension so that servers can accept client-to-server media uploads: [#754, #927]

    • Federation and FederationBuilder gained a setMediaUploader() method (through the new MediaUploaderSetters interface) that registers a multipart/form-data upload endpoint. Its callback finalizes the uploaded file alongside the posted object shell and returns either the created object (201 Created) or the URL at which it will become available once processing finishes (202 Accepted).
    • Context gained a getMediaUploaderUri() method for building the endpoint URI, which actor dispatchers advertise under the new Endpoints.uploadMedia property.
    • A new metric endpoint category, media_upload, classifies these requests in the fedify.endpoint attribute.
    • Fedify logs a runtime warning when a callback's returned URI does not point at a registered object dispatcher route, when a registered media uploader is not advertised under endpoints.uploadMedia, or when a media uploader is registered without an authorize() hook (so the endpoint would accept uploads from anyone).
  • Added a custom background task API that generalizes Fedify's enqueue-and-process-later pattern to arbitrary application-defined jobs:

    • Federation and FederationBuilder gained a defineTask() method through the new TaskRegistry interface, which Federatable now extends.
    • Context gained enqueueTask() and enqueueTaskMany() methods, with delay and orderingKey options (new TaskEnqueueOptions interface).
    • Every task requires a Standard Schema (schema option) from which the payload type is inferred; payloads are validated at enqueue time (fail fast) and again at dequeue time (protection against schema drift across deployments).
    • Payloads are serialized by Fedify with devalue, so Date, Map, Set, URL, bigint, circular references, and Activity Vocabulary objects round-trip faithfully across every message queue backend.
    • Failed handlers are retried with exponential backoff by default; tasks support per-task retryPolicy and onError options, the new FederationOptions.taskRetryPolicy sets the federation-wide default, and queues with nativeRetrial delegate retries to the backend.
    • Tasks can be isolated from activity delivery through the new FederationQueueOptions.task slot or a per-task queue option; without them, tasks fall back to the outbox queue unless the new FederationOptions.taskQueueResolution option is set to "strict". Federation.startQueue() now accepts queue: "task" to run a task-only worker.
    • Tasks can request at-most-once enqueue with a deduplicationKey (new TaskEnqueueOptions.deduplicationKey). A queue declaring the new MessageQueue.nativeDeduplication capability owns the check and receives the key through the new MessageQueueEnqueueOptions.deduplicationKey; otherwise Fedify performs a best-effort key–value guard through the optional KvStore.cas primitive, under a new taskDeduplication key prefix. The marker TTL and the no-cas fallback are tunable with the new FederationOptions.taskDeduplicationTtl and FederationOptions.taskDeduplicationFallback options. [#206, #797, #798, #799, #803, #806, #812, #923 by ChanHaeng Lee]
  • Added MessageQueue.atomicEnqueueMany for queues that implement enqueueMany() with separate sends. Fedify still uses their batch path normally, but rejects a multi-message batch governed by one deduplicationKey before a partial send can undermine deduplication. [#930, #934]

  • Fixed CommonJS distribution files that use Temporal so they no longer require @js-temporal/polyfill at runtime. The CommonJS build now bundles temporal-polyfill, while type declarations rely on the standard esnext.temporal lib reference. [#823, #925]

@fedify/adonisjs

  • Added the new @fedify/adonisjs package, an integration for the AdonisJS framework. It provides a server middleware that mounts a Federation inside an AdonisJS application, a service provider that owns the federation's lifecycle, a node ace configure hook that scaffolds config/fedify.ts and the federation preload files, and a ctx.federation request context. The package targets Node.js and is published to npm only. [#139, #1006 by Samuel Brinkmann]

@fedify/astro

  • Added and continuously tested support for Astro 6 and 7 while retaining Astro 5 compatibility. The Astro example and fedify init templates now use Astro 7 with current Node.js and Deno adapters; Bun uses the tested @astrojs/node standalone output instead of the Astro-5-only @nurodev/astro-bun adapter. [#931, #936]

@fedify/cli

  • Added [SvelteKit] option to fedify init command. This option allows users to initialize a new Fedify project with SvelteKit integration. [#892, #971 by Jang Hanarae]

  • Added fedify.com.es as a tunneling service. The CLI pins the service's SSH host key and rejects a mismatched server before exposing a local port. [#940]

  • Removed localhost.run as a tunneling service. The service is no longer available, and the CLI now rejects attempts to use it. [#940]

  • Switched the CLI's Temporal runtime dependency from @js-temporal/polyfill to temporal-polyfill. [#823, #925]

  • Added support for FEP-ef61 portable objects to the fedify command. [#288, #1156, #1199]

    • fedify lookup now looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the WebFinger handles of portable actors, and verifies their Object Integrity Proofs. Previously, it failed to look up portable IDs, and refused portable objects found through compatible identifiers as cross-origin objects. The same applies to the collections that -t/--traverse traverses and the objects that --recurse follows. When no gateway returns an acceptable object, the command tells why, e.g., that the object's proof is invalid.

    • Added the --gateway option to fedify lookup, fedify inbox, and fedify webfinger, which specifies the gateways to look up portable objects from, e.g., for portable IDs without @gateway location hints.

    • fedify inbox -f/--follow now follows portable actors, and the -a/--accept-follow option of fedify inbox and the -a/--accept-follow and -r/--reject-follow options of fedify relay accept portable IDs and compatible identifiers, which match the actor regardless of @gateway location hints and the gateway of a compatible identifier.

    • fedify webfinger now accepts portable actor IDs. As such an ID does not tell which server to ask, the command looks up the actor and then its WebFinger address, which consists of its preferredUsername and the host of its first gateway, and reports whether the response links back to the actor.

  • Fixed fedify lookup --recurse reporting a timeout or another network failure of the first object or of a linked object as a possibly private object, suggesting the -a/--authorized-fetch option. It now reports the actual cause, e.g., “Request timed out after 10 seconds,” like the other modes of fedify lookup do. [#1156, #1199]

  • Fixed the -p/--allow-private-address option of fedify webfinger being ignored. [#1156, #1199]

  • Changed fedify lookup to time out each request after 10 seconds when the -T/--timeout option is not given, since the document loaders it uses now have a default timeout. Previously, there was no timeout by default. The -T/--timeout option now also limits how long a request waits for a DNS lookup, though it cannot stop the lookup itself. [#1131, #1169]

  • Updated Optique to 1.3.2. This fixes several command-line parsing issues, so options with attached values such as --timeout=30 are handled consistently, typo suggestions for mistyped options are more accurate, and errors for known options are no longer hidden by positional arguments before --. [#1197]

@fedify/debugger

  • Fixed the CommonJS debugger build so it no longer requires @js-temporal/polyfill at runtime. The build now bundles temporal-polyfill, while type declarations rely on the standard esnext.temporal lib reference. [#823, #925]

@fedify/init

  • Added a test task to projects scaffolded by fedify init. It starts the app, waits for it to become ready, and checks that it resolves a local actor, giving projects a standard smoke test to run right after scaffolding and whenever the app changes afterwards. [#898, #990 by Jang Hanarae]
  • Added runtime version verification to fedify init. It checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a higher version required by a framework (such as Astro's Node.js 22.12), before generating a project. A missing, malformed, or unsupported runtime now produces a clear error in non-interactive mode and disables the affected package managers in interactive mode. [#964, #981 by Lee Jeongmin]
  • Fixed fedify init's hydration test validation to run format before format:check, which previously caused the entire test suite to fail when the package manager is npm or pnpm: [#950] [#952 by Jang Hanarae]
  • Supported [SvelteKit] as a web framework option in fedify init. [#892, #971 by Jang Hanarae]

@fedify/interaction-controls

  • Added the new @fedify/interaction-controls package for implementing GoToSocial interaction controls, FEP-044f, and FEP-7aa9. It provides immutable TypeScript APIs for creating and verifying interaction requests and authorizations, evaluating InteractionPolicy, recognizing bare interactions, and formatting stable storage keys for like, reply, announce, quote, and feature interactions. [#811, #929]

@fedify/lint

  • Added four lint rules for the media upload endpoint introduced in @fedify/fedify: [#754, #927]

    • media-uploader-object-uri-required warns when a setMediaUploader() callback does not derive its return value from ctx.getObjectUri().
    • media-uploader-authorization-required warns when setMediaUploader() is registered without an .authorize() hook.
    • actor-upload-media-property-required warns when a media uploader is registered but the actor dispatcher does not advertise endpoints.uploadMedia.
    • actor-upload-media-property-mismatch warns when endpoints.uploadMedia is not built with ctx.getMediaUploaderUri(identifier).
  • Added the actor-preferred-username-required lint rule, which warns when an actor dispatcher's return value does not include a preferredUsername property. [#895, #1022 by Jae-Hyuk-Jang]

  • Added the outbox-listener-delivery-not-awaited rule to @fedify/lint. It reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the returned promise, so that the activity may never leave on a runtime such as Cloudflare Workers, which discards pending work once the response is returned. A call counts as handled when its promise is awaited, returned, passed to Promise.all() or Promise.allSettled(), or handed to waitUntil(), and void, Promise.race() and Promise.any() are accepted as deliberate choices to stop waiting. The ESLint recommended configuration enables the rule as a warning and strict as an error, and Oxlint users enable it by name. It is not available in Deno Lint, which turns on every rule of a plugin at once. [#1057, #1067 by Jae-Hyuk-Jang]

  • Changed outbox-listener-delivery-required (@fedify/lint) to decide whether a ctx.sendActivity()/ctx.forwardActivity() call actually runs, instead of scanning the listener's source as a flat block of text. It now reports a listener whose only delivery calls sit behind a dead branch, after an unconditional return/throw, or inside a function that is never used. When it cannot tell whether a delivery call runs, it stays quiet: a function held under a name counts as used as soon as that name is mentioned, however it is passed around, and an inline callback counts wherever it is passed. [#900, #1050 by Jae-Hyuk-Jang]

  • Changed the actor URI mismatch rules to accept the FEP-ef61 portable IDs of actors and collections, and the compatible identifiers built from them, so that applications can use the getPortable*Uri() methods of Context in actor dispatchers without disabling these rules. [#288, #1157, #1179]

  • Fixed outbox-listener-delivery-required and outbox-listener-delivery-not-awaited (@fedify/lint) losing track of the functions an object already holds when a nested helper in the listener assigns another property of that object, as in target.fallback = () => {}. A listener that delivers through target.deliver() after such an assignment is no longer reported as undelivered, and a dropped delivery promise inside target.deliver() is now reported. A helper that declares its own object of the same name is still checked separately from the outer one. [#1125, #1140]

  • Fixed outbox-listener-delivery-required and outbox-listener-delivery-not-awaited (@fedify/lint) to properly evaluate reachability in statically false loops (like while (false)) and to correctly traverse for...of and for...in loop binding patterns. Unreachable loop branches no longer count as deliveries, and outbox-listener-delivery-not-awaited now correctly catches dropped promises inside loop binding patterns. Loop default functions are checked only when the bound value or target object is referenced. Delivery helpers used after an assignment-form loop default remain recognized, including when unrelated blocks reuse their names. [#1071, #1088 by @ArchieTansaria]

@fedify/mysql

  • Fixed the CommonJS MySQL adapter build so it no longer requires @js-temporal/polyfill at runtime. The build now bundles temporal-polyfill, while type declarations rely on the standard esnext.temporal lib reference. [#823, #925]

@fedify/netlify

  • Added NetlifyBlobsKvStore, a Netlify Blobs-backed key–value store with expiration, prefix listing, and atomic compare-and-set operations. Netlify deployments can now persist Fedify state and preserve ordered queue delivery without a separate database. [#1010, #1029 by Jiwon Kwon]
  • Added the new @fedify/netlify package for processing Fedify message queue jobs with Netlify Async Workloads. It provides NetlifyMessageQueue for durable event submission and createNetlifyQueueHandler() for Netlify Function consumers, including delayed delivery, native retry delegation, non-retryable malformed-event handling, durable per-key FIFO ordering, and explicit recovery for unobservable dead-letter failures. [#930, #934]

@fedify/next

  • Added support for FEP-ef61 hashlink media requests, e.g., GET /.well-known/apgateway/hl:zQm..., to fedifyWith(). Clients fetch such media with, e.g., Accept: image/*, so these requests were not passed to Fedify unless they had federation media types in their headers, and Next.js answered them instead of the hashlink media dispatcher. isFederationRequest() now recognizes them by their paths regardless of their headers. To make Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to the matcher of your middleware.ts or proxy.ts file. [#288, #1149, #1170]
  • Added isHashlinkMediaRequest() function. [#288, #1149, #1170]

@fedify/pglite

  • Added the @fedify/pglite package with PgliteKvStore, a KvStore backed by an embedded PGlite database. It accepts a caller-created PGlite instance and is intended for a single PGlite instance in a single runtime isolate; a message queue is not provided because PGlite does not share data between processes. [#1018, #1020 by ChanHaeng Lee]

@fedify/postgres

  • Added PostgresKvStore.cas(), including atomic creation, replacement, and deletion with TTL-aware comparison. This allows PostgreSQL-backed stores, including Netlify Database, to enforce queue ordering and other Fedify CAS operations. [#930, #934]
  • PostgresKvStore now creates crash-safe logged tables by default and migrates existing unlogged tables during initialization. Transient unlogged storage remains available with the unlogged option. The one-time migration rewrites and exclusively locks an existing table, so upgrades with large or busy key–value tables should schedule it accordingly. [#930, #934]
  • Fixed the CommonJS PostgreSQL adapter build so it no longer requires @js-temporal/polyfill at runtime. The build now bundles temporal-polyfill, while type declarations rely on the standard esnext.temporal lib reference. [#823, #925]

@fedify/redis

  • Added atomic RedisKvStore.cas() for standalone Redis and Redis Cluster. Redis-backed deployments can now use portable inbox forwarding and other features that need compare-and-swap. Custom codecs must encode equal values identically, and Redis must permit EVAL; deployments that deny scripting can no longer rely on the previous non-CAS fallback. [#1163, #1167]
  • Fixed the CommonJS Redis adapter build so it no longer requires @js-temporal/polyfill at runtime. The build now bundles temporal-polyfill, while type declarations rely on the standard esnext.temporal lib reference. [#823, #925]

@fedify/relay

  • Fixed the CommonJS relay build so it no longer requires @js-temporal/polyfill at runtime. The build now bundles temporal-polyfill, while type declarations rely on the standard esnext.temporal lib reference. [#823, #925]
  • Fixed the relay documentation to use the canonical actor and shared inbox URIs, distinguish Mastodon-style and LitePub-style subscription behavior, and explain which deployment responsibilities remain with applications. [#899, #996 by Jiwon Kwon]

@fedify/sqlite

  • Fixed the CommonJS SQLite adapter build so it no longer requires @js-temporal/polyfill at runtime. The build now bundles temporal-polyfill, while type declarations rely on the standard esnext.temporal lib reference. [#823, #925]

@fedify/testing

  • Added support for FEP-ef61 portable objects to the mock federation and contexts, following the new APIs of @fedify/fedify, so that tests can exercise portable objects without a live gateway. [#288]

    • Added the getPortableActorUri(), getPortableObjectUri(), getPortableInboxUri(), getPortableOutboxUri(), getPortableFollowingUri(), getPortableFollowersUri(), getPortableLikedUri(), getPortableFeaturedUri(), getPortableFeaturedTagsUri(), and getPortableCollectionUri() methods to the mock contexts. They reject did:key DIDs that are not encoded in base58-btc. [#835, #839, #841, #1092, #1111, #1114, #1142]

    • Added the portable option to parseUri() of the mock contexts. Without it, parseUri() no longer recognizes a portable ID or compatible identifier whose path starts with /users/; with it, the result has the DID in its authority property. It also returns null for null. [#1143, #1145]

    • The mock contexts that createContext() creates keep the verifyPortableObject property given to them, and their lookupObject() and traverseCollection() pass it on. createFederation() accepts a fixture documentLoader, a contextLoader, and verifyPortableObject, which mock context lookups use for portable IDs. [#1107, #1120, #1161, #1196]

    • Added the isSignedByAudience() method to the mock request contexts, which checks the audience against the actor that getSignedKeyOwner() returns. [#1153, #1183]

    • federation.createContext() accepts an explicit portableRequest for a request context, which dispatchers can inspect. [#1161, #1196]

    • Added the mapPortableActorId() method to the setters that MockFederation.setActorDispatcher() returns, the mapPortableOwner() method to the mock custom collection setters, and the setHashlinkMediaDispatcher() method to the mock federation, which serves hashlink media responses. [#838, #840, #1080, #1099, #1111, #1142, #1161, #1196]

  • Added support for registering onRequestFinished() on mock federations so applications can reuse their inbox configuration in tests. [#1191, #1201]

  • Added testKvStore(), a conformance test suite for KvStore implementations, complementing testMessageQueue(). [#1018, #1020 by ChanHaeng Lee]

  • Changed getObject() of the mock context that createFederation() creates to follow RequestContext.getObject() of @fedify/fedify: it now returns null for a Tombstone that the object dispatcher returns, unless the tombstone is an instance of the requested class or { tombstone: "passthrough" } is given. [#1112, #1117]

  • Fixed the CommonJS testing utilities build so it no longer requires @js-temporal/polyfill at runtime. The build now bundles temporal-polyfill, while type declarations rely on the standard esnext.temporal lib reference. [#823, #925]

@fedify/vocab

  • Added support for FEP-ef61 portable objects, whose IDs are ap: or ap+ef61: URIs with a DID instead of a host, e.g., ap://did:key:z6Mk.../actor, and which are retrieved through the servers listed in their actors' gateways. See the Portable objects chapter of the manual for the FEP-ef61 profile that Fedify supports. [#288]

    • Generated vocabulary classes now accept portable IDs with decoded or percent-encoded DID authorities wherever an IRI is expected, and serialize them as canonical ap+ef61: IRIs with decoded DID authorities. Inspecting vocabulary objects, e.g., with console.log(), also shows them in this form. [#826, #850, #1156, #1199]

    • Added the gateways property to actor classes, and the digestMultibase property to Link and document and media classes. Gateways are serialized as origins without a trailing slash. A portable actor that uses the gateways term without mapping it in its JSON-LD context, as tootik does, is also read. [#830, #928, #1097, #1202]

    • Added the optional Recipient.gateways property, through which Fedify delivers activities to portable inboxes. Applications that build Recipient objects by hand, e.g., in followers collection dispatchers, need to set it for portable actors. [#1147, #1180]

    • Property accessors such as Create.getObject() now fetch portable references through gateways: those in the new gateways option, or else those in the @gateway location hints of the reference. A reference without hints that has the same DID as a portable actor it was reached from, e.g., the actor's outbox, is fetched through that actor's gateways. A fetched object is returned only if its @id identifies the referenced portable object and the verifier given as the new verifyPortableObject option accepts it, typically verifyPortableObject() from @fedify/fedify, or a Context passed as the options. Portable references cannot be dereferenced without the option, and crossOrigin: "trust" does not skip these checks. [#834, #1077, #1093, #1105]

    • Property accessors tell the verifyPortableObject function where a portable object was retrieved from and which objects led to it, so that it can accept a portable collection without a proof if a gateway that its owner lists served it. Objects embedded in such a collection are fetched and verified one by one. Added the crossOrigin, gateways, and verifyPortableObject options to TraverseCollectionOptions as well. [#836, #1084]

    • Added the verifyPortableObject option to the constructors, the fromJsonLd() methods, and the clone() methods of vocabulary classes, which sets the verifier that the object's property accessors use by default. Objects embedded in the parsed document, and objects that accessors and traverseCollection() fetch, get the verifier of the call or of their parent by default, so that, e.g., (await create.getObject(ctx))?.getAttribution() verifies portable objects without passing ctx again. Having a default verifier does not mean that an object was verified. Added the inheritPortableObjectVerifier option to property accessors and TraverseCollectionOptions to limit a verifier to a single call. [#1107, #1120, #1129, #1137]

    • Added the verifyPortableObject and gateways options to LookupObjectOptions. With the former, lookupObject() looks up portable IDs through their @gateway location hints or the gateways given by the latter, compatible identifiers through the gateways they name, and the handles of portable actors through their WebFinger responses, trying at most five gateways per lookup. The gateways that it infers from compatible identifiers, WebFinger responses, and location hints are passed to the verifyPortableObject function as gatewayHints, while gateways has the ones given explicitly. [#837, #1082, #1090, #1091, #1158, #1194]

    • getActorHandle() now supports portable actors. It takes the domain of a portable actor's handle from the first gateway in its gateways, and returns the handle only if its WebFinger response links back to the actor. [#837, #1082]

    • Exported the portable object verifier types and other types used in vocabulary API signatures from @fedify/vocab, so that custom verifiers can be typed without importing @fedify/vocab-runtime. [#1160, #1184]

  • Changed property accessors and lookupObject() so that they no longer trust FEP-ef61 compatible identifiers, i.e., HTTP(S) URLs under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor, because of the origin that serves them. Anyone can serve a compatible identifier for any DID, so previously anyone could serve an unsigned object that claimed to be someone else's portable object. [#288, #837, #1082, #1090, #1091, #1107, #1120]

    • With the verifyPortableObject option, a compatible identifier is dereferenced as the portable object it stands for: through the gateway that it names and then the gateways in the gateways option, only if the option accepts the object. A document fetched from an ordinary HTTP(S) URL is verified the same way if its final URL is a compatible identifier or its @id is a portable ID. Malformed compatible identifiers are rejected without a request.

    • Without the option, accessors keep fetching compatible identifiers as ordinary HTTP(S) URLs, but no longer cache the results in the parent objects, so that a later call with the option verifies them. However, accessors of an object whose ID is a portable ID or a compatible identifier now throw a TypeError (or return null with suppressError: true) for such references without the option, as for portable IDs.

    • Accessors no longer trust an embedded object whose @id is a compatible identifier, or a portable ID with a DID other than its parent's, even with crossOrigin: "trust"; they dereference and verify it on its own. Likewise, crossOrigin: "trust" no longer makes lookupObject() return an object with a portable @id from a document URL of another origin.

  • Updated FEP-fe34 cross-origin checks to understand the cryptographic origins of FEP-ef61 portable IDs and DID URLs. Property accessors and lookupObject() now treat ap: and ap+ef61: IDs and did:key verification method IDs as the same origin when their DIDs match. [#288, #829, #926]

  • Changed nested serialization so that an object carrying the signed JSON-LD representation that signObject() retains is embedded with that exact representation, including its own @context, rather than being rebuilt under the parent's context, so that its proof keeps verifying. clone() never carries the retained representation, because a clone may differ from the document the proof covers. [#288, #1044, #1051]

  • Added vocabulary support for FEP-7aa9, including FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, plus actor featuredCollections and InteractionPolicy.canFeature properties. [#810, #914]

  • Added the Endpoints.uploadMedia property, the standard ActivityStreams endpoint for the ActivityPub Media Upload extension. [#754, #927]

  • Fixed the CommonJS vocabulary build so it no longer requires @js-temporal/polyfill at runtime. The build now bundles temporal-polyfill, while type declarations rely on the standard esnext.temporal lib reference. [#823, #925]

  • Added vocabulary support for the FEP-22cd draft, associating each translated version with its translators, source object, and optional source review timestamp. [#1037, #1038]

    • Added Translation class with id, language, original, sourceUpdated, basis, url, and urls properties.
    • Added Translation.getTranslator()/Translation.translatorId and Translation.getTranslators()/Translation.translatorIds for accessing credited actors. The constructor accepts translator and translators values.
    • Added Object.translations property, inherited by Article, Note, and other object types, for per-language translation metadata without creating separate posts.

@fedify/vocab-runtime

  • Added https://w3id.org/fep/22cd to preloaded JSON-LD contexts. [#1037, #1038]

  • Added support for FEP-ef61 portable objects, whose IDs are ap: or ap+ef61: URIs with a DID instead of a host, e.g., ap://did:key:z6Mk.../actor. See the Portable objects chapter of the manual for the FEP-ef61 profile that Fedify supports. [#288]

    • Added parseIri(), formatIri(), parseJsonLdId(), and haveSameIriOrigin(), which parse, format, and compare IRIs, including portable IDs with decoded or percent-encoded DID authorities. The URL objects that represent portable IDs keep their DIDs percent-encoded, and formatIri() formats them in their canonical form, e.g., ap+ef61://did:key:z6Mk.../actor. Portable IDs and compatible identifiers whose paths have . or .. segments, which the URL class would remove, are rejected with a TypeError. [#826, #850, #1154, #1186]

    • Added canonicalizePortableUri() and arePortableUrisEqual() for comparing portable IDs. They accept both schemes with decoded or percent-encoded DID authorities, normalize them to ap+ef61:, and ignore the query, including @gateway location hints. Fedify deliberately canonicalizes to ap+ef61: rather than ap:, which FEP-ef61 currently recommends, and this may change in Fedify 3.0, so compare portable IDs with arePortableUrisEqual() rather than as strings. [#828, #924, #1151, #1198]

    • Added getFe34Origin() and haveSameFe34Origin(), which compare FEP-fe34 origins: HTTP(S) URLs have their web origins, while portable IDs and DID URLs have their DIDs as their cryptographic origins. [#829, #926]

    • Added exportDidKey(), importDidKey(), and parseDidKeyVerificationMethod() for Ed25519 did:key DIDs and their verification method DID URLs. exportDidKey() always encodes DIDs in base58-btc, as FEP-ef61 requires. [#827, #915]

    • Added toCompatibleEf61Id() and fromCompatibleEf61Id() for converting between portable IDs and compatible identifiers, i.e., HTTP(S) URLs under a gateway's /.well-known/apgateway/ path such as https://example.com/.well-known/apgateway/did:key:z6Mk.../actor, which software without portable ID support can use. fromCompatibleEf61Id() returns null for URLs that are not compatible identifiers, and throws a TypeError for malformed ones, including those with location hints. Converting a compatible identifier does not authenticate it. [#833, #1074]

    • Added isGatewayUrl() and parseGatewayUrl(), which check that a gateway is an HTTP(S) origin without credentials, a path, a query, or a fragment, as FEP-ef61 requires. [#830, #928, #1176, #1190]

    • Added withGatewayHints(), withoutGatewayHints(), and getGatewayHints() to add, remove, and read the @gateway location hints of portable IDs, which tell consumers where to retrieve a referenced portable actor. [#1159, #1189]

    • Added SHA-256 digestMultibase and hl: hashlink helpers: computeDigestMultibase(), parseDigestMultibase(), verifyDigestMultibase(), createHashlink(), parseHashlink(), and verifyHashlink(). [#831, #935]

    • Added fetchPortableMedia(), which retrieves the media of a portable object through its owner's gateways, or an HTTP(S) resource directly, and returns it only after verifying its digestMultibase. It limits the response size and rejects private network addresses by default. [#1152, #1182]

    • Added the FEP-ef61 JSON-LD context to the preloaded contexts, so that gateways and digestMultibase can be compacted and expanded without fetching the context. [#830, #928]

    • Added the PortableObjectVerifier, PortableObjectVerifierOptions, PortableObjectVerification, and PortableObjectReferrer types, which describe the verifyPortableObject option of property accessors. A verifier receives a fetched document along with its final URL, the gateways used, and the chain of objects that referred to it, and can accept a collection without a proof as unsecured. [#834, #836, #1077, #1084]

    • Added the verifyPortableObject property to PropertyPreprocessorContext, the default verifier that objects returned by a property preprocessor should use. [#1107, #1120]

  • Changed the Accept header that document loaders send when fetching ActivityPub objects to application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", as ActivityPub and FEP-ef61 gateways require. Previously, the JSON-LD media type lacked the ActivityStreams profile. [#288, #834, #1077]

  • Added the FEP-7aa9 JSON-LD context to the preloaded context registry so FEP-7aa9 documents can be compacted and expanded without fetching the context remotely. [#810, #914]

  • Changed getDocumentLoader() to reject HTML and XHTML responses that do not advertise an ActivityPub alternate document with a FetchError instead of attempting to parse the HTML as JSON. This makes remote HTML error pages surface as document loading failures with the response URL and content type, rather than generic JSON parser crashes. [#912, #913]

  • Changed getDocumentLoader() to time out each call after 10 seconds by default. The time limit covers the whole call, including every redirect and alternate document it follows and reading the response body. A call that times out throws a FetchError without a response, whose cause is a DOMException named "TimeoutError". An AbortSignal passed as the signal option still cancels a call as before. [#1131, #1169]

    • Added DocumentLoaderFactoryOptions.timeout option, in milliseconds, to change the time limit, or to turn it off with null.
  • Changed getDocumentLoader() to read the body of an error response before it throws a FetchError, at most 1 MiB, so that the time limit also bounds reading it. FetchError.response keeps the body byte for byte, or only the status and headers if the body is larger than that. [#1131, #1169]

@fedify/vocab-tools

  • Added the extraContext property schema option to include a JSON-LD context only when its terms are used, preserving existing output for objects without the extension. [#1037, #1038]
  • Added the trustEmbeddedObjects type schema option so embedded metadata identifiers need not establish trust in linked actors. [#1037, #1038]
  • Changed suppressed vocabulary fetch and parsing failures to log at the warning level so that intentionally handled failures are not reported as application errors. [#933, #1035 by Jae Hui Hong]