Skip to content
Merged
38 changes: 38 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ To be released.
as a remote JSON-LD document, which is required for [FEP-ef61]
portable objects. [[#827], [#915]]

- 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, or when a registered
media uploader is not advertised under `endpoints.uploadMedia`.

- Added a custom background task API that generalizes Fedify's
enqueue-and-process-later pattern to arbitrary application-defined jobs:

Expand Down Expand Up @@ -70,8 +88,10 @@ To be released.
[FEP-8b32]: https://w3id.org/fep/8b32
[FEP-fe34]: https://w3id.org/fep/fe34
[FEP-ef61]: https://w3id.org/fep/ef61
[ActivityPub Media Upload extension]: https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload
[Standard Schema]: https://standardschema.dev/
[#206]: https://github.com/fedify-dev/fedify/issues/206
[#754]: https://github.com/fedify-dev/fedify/issues/754
[#797]: https://github.com/fedify-dev/fedify/issues/797
[#798]: https://github.com/fedify-dev/fedify/issues/798
[#799]: https://github.com/fedify-dev/fedify/issues/799
Expand All @@ -85,6 +105,7 @@ To be released.
[#923]: https://github.com/fedify-dev/fedify/pull/923
[#925]: https://github.com/fedify-dev/fedify/pull/925
[#926]: https://github.com/fedify-dev/fedify/pull/926
[#927]: https://github.com/fedify-dev/fedify/pull/927

### @fedify/vocab

Expand All @@ -105,6 +126,9 @@ To be released.
`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
Expand Down Expand Up @@ -205,6 +229,20 @@ To be released.
`temporal-polyfill`, while type declarations rely on the standard
`esnext.temporal` lib reference. [[#823], [#925]]

### @fedify/lint

- Added three 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()`.
- `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)`.


Version 2.3.1
-------------
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ const MANUAL = {
{ text: "Actor dispatcher", link: "/manual/actor.md" },
{ text: "Inbox listeners", link: "/manual/inbox.md" },
{ text: "Outbox listeners", link: "/manual/outbox.md" },
{ text: "Media upload", link: "/manual/media-upload.md" },
{ text: "Sending activities", link: "/manual/send.md" },
{ text: "Collections", link: "/manual/collections.md" },
{ text: "Conversation backfill", link: "/manual/backfill.md" },
Expand Down
7 changes: 7 additions & 0 deletions docs/manual/actor.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,13 @@ const ctx = null as unknown as Context<void>;
new Endpoints({ sharedInbox: ctx.getInboxUri() })
~~~~

If you register a media uploader with `Federation.setMediaUploader()`, advertise
it here too, under the `uploadMedia` endpoint, using the
`Context.getMediaUploaderUri()` method. See the [*Media upload*][media-upload]
guide for details.

[media-upload]: ./media-upload.md

### `publicKey`

The `publicKey` property contains the public key of the actor. It is
Expand Down
1 change: 1 addition & 0 deletions docs/manual/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ shows the methods:
- `~Context.getObjectUri()`
- `~Context.getInboxUri()`
- `~Context.getOutboxUri()`
- `~Context.getMediaUploaderUri()`
- `~Context.getFollowingUri()`
- `~Context.getFollowersUri()`
- `~Context.getLikedUri()`
Expand Down
128 changes: 128 additions & 0 deletions docs/manual/lint.md
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,48 @@ federation
});
~~~~

### `media-uploader-object-uri-required`

Warns when a `setMediaUploader()` callback returns a value that is not derived
from `ctx.getObjectUri()`.

**When this rule applies:**
You've registered a media uploader with `setMediaUploader()`, but its callback
never references `ctx.getObjectUri()`, so the returned `id`/`URL` probably does
not point at a registered object dispatcher route.

**Why it matters:**
The upload endpoint emits object IDs, but serving those IDs back as fetchable
ActivityStreams objects is the developer's job via `setObjectDispatcher()`.
Deriving the returned value from `ctx.getObjectUri()` keeps the emitted ID in
sync with a registered object dispatcher route.

~~~~ typescript twoslash
// @noErrors: 2345
import { createFederation } from "@fedify/fedify";
import { Image } from "@fedify/vocab";
const federation = createFederation<void>({ kv: null as any });
// ---cut-before---
// ❌ Bad: Returned id is a hard-coded URL, not from ctx.getObjectUri()
federation.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
return new Image({ id: new URL("https://example.com/media/1") });
},
);

// ✅ Good: Returned id is derived from ctx.getObjectUri()
federation.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
return new Image({
id: ctx.getObjectUri(Image, { uuid: "1" }),
mediaType: file.type,
});
},
);
~~~~

### `actor-followers-property-required`

Ensures `followers` is defined when `setFollowersDispatcher()` is configured.
Expand Down Expand Up @@ -1219,6 +1261,92 @@ federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => {
});
~~~~

### `actor-upload-media-property-required`

Ensures `endpoints.uploadMedia` is defined when `setMediaUploader()` is
configured.

**When this rule applies:**
You've called `federation.setMediaUploader()`, but the actor object doesn't
advertise the endpoint under an
`endpoints: new Endpoints({ uploadMedia: ... })` property.

**Why it matters:**
Registering a media uploader does not by itself expose the endpoint to clients.
Advertising it under `endpoints.uploadMedia` is what lets clients discover where
to upload media.

~~~~ typescript twoslash
// @noErrors: 2345
import { createFederation } from "@fedify/fedify";
import { Endpoints, Person } from "@fedify/vocab";
const federation = createFederation<void>({ kv: null as any });
// ---cut-before---
// ❌ Bad: Missing endpoints.uploadMedia when a media uploader is registered
federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => {
return new Person({
id: ctx.getActorUri(identifier),
// Missing endpoints.uploadMedia!
});
});

federation.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) =>
ctx.getObjectUri(Person, { uuid: "1" }),
);

// ✅ Good: Advertise endpoints.uploadMedia
federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => {
return new Person({
id: ctx.getActorUri(identifier),
endpoints: new Endpoints({
uploadMedia: ctx.getMediaUploaderUri(identifier),
}),
});
});
~~~~

### `actor-upload-media-property-mismatch`

Validates that `endpoints.uploadMedia` is set using
`ctx.getMediaUploaderUri(identifier)`.

**When this rule applies:**
The `endpoints.uploadMedia` property is set to a value other than
`ctx.getMediaUploaderUri(identifier)`.

**Why it matters:**
The advertised upload endpoint URI must match the path configured in
`setMediaUploader()`, or clients will upload to the wrong URL.

~~~~ typescript twoslash
// @noErrors: 2345
import { createFederation } from "@fedify/fedify";
import { Endpoints, Person } from "@fedify/vocab";
const federation = createFederation<void>({ kv: null as any });
// ---cut-before---
// ❌ Bad: Using a hard-coded URL for the upload endpoint
federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => {
return new Person({
id: ctx.getActorUri(identifier),
endpoints: new Endpoints({
uploadMedia: new URL("https://example.com/upload"), // Wrong!
}),
});
});

// ✅ Good: Use ctx.getMediaUploaderUri(identifier)
federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => {
return new Person({
id: ctx.getActorUri(identifier),
endpoints: new Endpoints({
uploadMedia: ctx.getMediaUploaderUri(identifier),
}),
});
});
~~~~

### `collection-filtering-not-implemented`

Warns when collection dispatchers don't implement filtering.
Expand Down
Loading