Skip to content

Support the ActivityPub Media Upload extension#927

Merged
dahlia merged 11 commits into
fedify-dev:mainfrom
dahlia:media-upload-ext
Jul 9, 2026
Merged

Support the ActivityPub Media Upload extension#927
dahlia merged 11 commits into
fedify-dev:mainfrom
dahlia:media-upload-ext

Conversation

@dahlia

@dahlia dahlia commented Jul 8, 2026

Copy link
Copy Markdown
Member

Client-to-server support arrived in Fedify 2.2 with setOutboxListeners() (#430), which lets an application accept ActivityPub C2S activities without exposing a bespoke API. But posting an activity is only half of what C2S clients need: they also upload binary media (images, video, audio), and that traffic deliberately does not travel through POST /outbox. The ActivityPub Media Upload extension exists precisely to fill this gap, so it is the natural next step now that the posting surface is in place. This implements it (#754).

Why it mirrors the outbox

The API intentionally follows the shape that setOutboxListeners() established, because the two endpoints are the same kind of thing: an authenticated C2S entry point whose server-side behavior is the application's business, not the framework's. setMediaUploader(path, callback) returns a setter with a single .authorize() hook, authorization runs before the callback, and a rejection produces 401 (overridable through the federation-level onUnauthorized), exactly as on the outbox. Keeping the surface familiar means anyone who already wired up outbox listeners can reason about media upload without learning a second mental model, and it preserves Fedify's consistent division of labor across C2S: the framework routes and authorizes, the application decides what to store and serve.

Why the endpoint is advertised explicitly rather than auto-injected

The endpoint is exposed through a new Context.getMediaUploaderUri(identifier), and the actor dispatcher places it under the new Endpoints.uploadMedia property itself:

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

An earlier design auto-injected endpoints.uploadMedia into the actor document. It was dropped because it would be inconsistent with the rest of the framework: getInboxUri(), getOutboxUri(), and their siblings only expose URLs, while deciding what actually appears on the actor document is the actor dispatcher's responsibility. Silently writing one endpoint but not the others would be a surprising special case that erodes that rule. Instead, the framework warns at runtime, and @fedify/lint warns statically, when an uploader is registered but the actor fails to advertise the matching URI: the convention stays uniform, and the omission is still caught.

Why the callback returns Object | URL

The return type encodes the one decision the framework cannot make on the application's behalf: whether the uploaded resource is fetchable immediately or is still being processed. Returning an Object means “ready now”, so Fedify answers 201 Created with Location pointing at the object's id; returning a URL means “it will live here once processing finishes” (for example after transcoding), so it answers 202 Accepted with Location set to that URL. This is purely the HTTP-level distinction between 201 and 202, independent of whether the callback happens to be asynchronous. The type therefore accepts a plain value or a Promise, matching how every other value-returning Fedify callback behaves, rather than forcing a Promise return just because the realistic implementation awaits storage.

Why missing actors and id-less results are rejected

Two failure modes are closed off before they can turn into a misleading success. First, the handler resolves the actor dispatcher up front and returns 404, without ever invoking the callback, when the actor is absent or a Tombstone. The actor dispatcher is Fedify's source of truth for which actors exist, just as on the outbox path; without this guard an application could store media and return 201/202 for a user that never existed or was deleted. Second, an Object returned without an id produces 500 instead of a 201 that lacks Location. A 201 Created with no Location is unusable to the client (it cannot discover the object it just created), so failing loudly is strictly better than succeeding uselessly.

How the request is handled

The endpoint is registered as a mediaUploader route and handled before JSON-LD content negotiation, because a multipart/form-data upload has nothing to negotiate; this placement also lets any non-POST method resolve cleanly to 405 instead of a confusing negotiation 406. From there the handler runs body-state guards, a content-type check, actor resolution, authorization, parsing, and the callback. It deliberately resolves the actor before authorization, unlike the outbox: a request for a nonexistent or deleted actor receives 404 regardless of the authorize hook, and the upload body is not buffered for a bogus identifier (the actor's existence is already public via its actor URI, so this leaks nothing new). A new media_upload value in the fedify.endpoint metric attribute keeps these requests distinguishable in telemetry rather than lumping them into not_found.

Why the linting is shaped the way it is

Runtime warnings only fire once the offending code path actually executes, so the three new @fedify/lint rules move the same guarantees to development time. The two actor-side rules (actor-upload-media-property-required and actor-upload-media-property-mismatch) reuse the existing property-checking machinery and therefore behave like the established sharedInbox rules. The object-side rule (media-uploader-object-uri-required) is where the real care went, because “the returned id must derive from getObjectUri()“ is easy to state and easy to get subtly wrong. It inspects the returned expressions specifically, not the whole callback body, so a getObjectUri() call sitting in a dead branch cannot excuse a hard-coded return. For object returns it judges the effective id alone (the last id property wins, and a trailing spread is treated as unsafe), so a getObjectUri() call in some unrelated property such as url cannot launder a hard-coded id. And it requires the call's receiver to be the callback's own context parameter, so helper.getObjectUri(...) on some other object is not mistaken for the genuine one.

What is intentionally left to the application

Consistent with the philosophy adopted in #430, several responsibilities stay with the developer: storing the file, serving the uploaded object back as fetchable ActivityStreams (that is what setObjectDispatcher() is for), wrapping the result in a Create, and capping the upload size. On that last point, Fedify reads the multipart body into memory and imposes no size limit of its own, the same as the inbox and outbox. Rather than bolt on a one-off limit option, the documentation directs applications to enforce a maximum at the deployment layer (for example a reverse proxy's body-size setting), which is where Fedify already expects that boundary to sit.

Documentation

A new docs/manual/media-upload.md walks through the whole flow (registering the uploader, the 201/202 semantics, advertising the endpoint, and the error responses), and getMediaUploaderUri(), the endpoints.uploadMedia property, and the three lint rules are documented next to their existing siblings.

@dahlia dahlia added this to the Fedify 2.4 milestone Jul 8, 2026
@dahlia dahlia self-assigned this Jul 8, 2026
@dahlia dahlia requested review from 2chanhaeng and sij411 as code owners July 8, 2026 17:20
@dahlia dahlia added component/federation Federation object related activitypub/compliance Specification compliance component/lint Lint related (@fedify/lint) labels Jul 8, 2026
@netlify

netlify Bot commented Jul 8, 2026

Copy link
Copy Markdown

Deploy Preview for fedify-json-schema canceled.

Name Link
🔨 Latest commit 8fdd9a0
🔍 Latest deploy log https://app.netlify.com/projects/fedify-json-schema/deploys/6a4f57bc2f43370008a2cfb3

@dahlia dahlia linked an issue Jul 8, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds ActivityPub media upload support across federation APIs, request handling, vocab, lint rules, tests, docs, and changelog entries. It introduces the media upload endpoint, URI helpers, endpoint advertisement, and validation around callback and actor configuration.

Changes

ActivityPub Media Upload feature

Layer / File(s) Summary
Contracts and schema
packages/fedify/src/federation/callback.ts, context.ts, federation.ts, packages/vocab/src/endpoints.yaml, vocab.test.ts
Adds the media uploader callback type, context URI helper, federation API surface, and the uploadMedia vocabulary property with round-trip coverage.
Builder registration and validation
packages/fedify/src/federation/builder.ts, builder.test.ts
Adds media uploader builder state, route registration, build-time wiring, actor advertisement warnings, and path validation tests.
Request handling and routing
packages/fedify/src/federation/handler.ts, middleware.ts, middleware.test.ts, packages/fedify/src/testing/context.ts, packages/testing/src/context.ts, packages/testing/src/mock.ts
Adds the media upload handler, routes POST requests to it, assigns the media_upload endpoint category, exposes ContextImpl.getMediaUploaderUri(), and extends testing helpers and middleware tests for request flow and warnings.
Lint rules and registration
packages/lint/src/lib/const.ts, rules/*upload-media*, media-uploader-object-uri-required.ts, index.ts, mod.ts, oxlint.ts, tests/*, README.md
Adds uploadMedia rule metadata, three rule modules, plugin registration, and rule tests for media uploader validation.
Documentation and changelog
docs/manual/media-upload.md, actor.md, context.md, lint.md, opentelemetry.md, .vitepress/config.mts, CHANGES.md
Adds the media upload manual page, sidebar entry, related docs, OpenTelemetry update, lint docs, and changelog entries.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

  • fedify-dev/fedify#758: Shares route-template validation and RouterError plumbing that the new media uploader route registration relies on.

Suggested labels: type/feature, component/integration, activitypub/interop

Suggested reviewers: 2chanhaeng

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding ActivityPub Media Upload support.
Description check ✅ Passed The description is detailed and directly describes the media upload extension changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@dahlia

dahlia commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

@codex review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for the ActivityPub Media Upload extension, allowing servers to accept client-to-server media uploads. It adds the setMediaUploader method to register a multipart/form-data upload endpoint, updates the Context to build the endpoint URI, and exposes the Endpoints.uploadMedia property. Additionally, three new lint rules are added to @fedify/lint to validate correct configuration. A review comment suggests adding a defensive check in the request handler to prevent a potential runtime crash if the media uploader callback returns a nullish value.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/fedify/src/federation/handler.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6784f2edb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/lint/src/rules/media-uploader-object-uri-required.ts
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.22905% with 168 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...nt/src/rules/media-uploader-object-uri-required.ts 77.41% 41 Missing and 48 partials ⚠️
packages/fedify/src/federation/handler.ts 82.92% 29 Missing and 6 partials ⚠️
packages/testing/src/mock.ts 10.34% 26 Missing ⚠️
packages/fedify/src/federation/builder.ts 79.06% 8 Missing and 1 partial ⚠️
...src/rules/media-uploader-authorization-required.ts 94.33% 3 Missing and 3 partials ⚠️
packages/fedify/src/federation/middleware.ts 93.33% 2 Missing and 1 partial ⚠️
Files with missing lines Coverage Δ
packages/fedify/src/testing/context.ts 73.75% <100.00%> (+0.33%) ⬆️
packages/lint/src/index.ts 100.00% <100.00%> (ø)
packages/lint/src/lib/const.ts 100.00% <100.00%> (ø)
packages/lint/src/mod.ts 100.00% <100.00%> (ø)
.../src/rules/actor-upload-media-property-mismatch.ts 100.00% <100.00%> (ø)
.../src/rules/actor-upload-media-property-required.ts 100.00% <100.00%> (ø)
packages/testing/src/context.ts 52.33% <100.00%> (+0.49%) ⬆️
packages/fedify/src/federation/middleware.ts 83.68% <93.33%> (+0.11%) ⬆️
...src/rules/media-uploader-authorization-required.ts 94.33% <94.33%> (ø)
packages/fedify/src/federation/builder.ts 62.37% <79.06%> (+0.73%) ⬆️
... and 3 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/fedify/src/federation/handler.ts`:
- Around line 1069-1081: The multipart parsing in handler.ts via
request.formData() has no built-in size or timeout cap, so add a documented
recommendation or configuration hook for deployers to enforce request-size
limits at the reverse-proxy/runtime layer. Update the handling around the
request.formData() path in the federation handler to make the limitation
explicit, and if possible expose a knob or note near the multipart parsing logic
so operators can bound uploads before they reach request.formData().
- Around line 1164-1176: The `handleMediaUpload` success path is missing error
handling around `result.toJsonLd(ctx)`, so a serialization/context-loader
failure escapes uncaught. Wrap the `toJsonLd` call in the same guarded
error-to-response flow used elsewhere in `handler.ts` (the multipart parse,
field validation, and callback failure paths), and convert any thrown exception
into a controlled 500-style response instead of letting it propagate through
`FederationImpl.fetch()`.

In `@packages/fedify/src/federation/middleware.test.ts`:
- Around line 3889-3914: The upload test helper only covers the `object` field
as a `Blob`, so the `handleMediaUpload` plain-string branch is untested. Add a
dedicated test in `middleware.test.ts` that builds a `FormData` with `file` plus
`object` appended as a raw JSON string, then calls the federation upload
endpoint and asserts the request is accepted. Use the existing
`createFederation`, `setActorDispatcher`, and `setMediaUploader` setup to place
the test near the current media upload coverage and explicitly exercise `typeof
objectField === "string"`.

In `@packages/lint/src/index.ts`:
- Around line 66-77: The new rule exports in the lint index are wired into rules
but are not enabled by the recommended config yet. Update recommendedRuleIds in
the index module to include the new rule identifiers alongside actorIdRequired
and actorIdMismatch if they should be part of the recommended preset, and verify
the symbol names match the imported rule modules such as
actorUploadMediaPropertyMismatch, actorUploadMediaPropertyRequired,
collectionFiltering, and mediaUploaderObjectUriRequired.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 517bf533-09de-4a78-bf40-c9661f49ede7

📥 Commits

Reviewing files that changed from the base of the PR and between 7b19967 and b6784f2.

⛔ Files ignored due to path filters (4)
  • packages/vocab-tools/src/__snapshots__/class.test.ts.deno.snap is excluded by !**/*.snap
  • packages/vocab-tools/src/__snapshots__/class.test.ts.node.snap is excluded by !**/*.snap
  • packages/vocab-tools/src/__snapshots__/class.test.ts.snap is excluded by !**/*.snap
  • packages/vocab/src/__snapshots__/vocab.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (31)
  • CHANGES.md
  • docs/.vitepress/config.mts
  • docs/manual/actor.md
  • docs/manual/context.md
  • docs/manual/lint.md
  • docs/manual/media-upload.md
  • docs/manual/opentelemetry.md
  • packages/fedify/src/federation/builder.test.ts
  • packages/fedify/src/federation/builder.ts
  • packages/fedify/src/federation/callback.ts
  • packages/fedify/src/federation/context.ts
  • packages/fedify/src/federation/federation.ts
  • packages/fedify/src/federation/handler.ts
  • packages/fedify/src/federation/middleware.test.ts
  • packages/fedify/src/federation/middleware.ts
  • packages/fedify/src/testing/context.ts
  • packages/lint/README.md
  • packages/lint/src/index.ts
  • packages/lint/src/lib/const.ts
  • packages/lint/src/mod.ts
  • packages/lint/src/oxlint.ts
  • packages/lint/src/rules/actor-upload-media-property-mismatch.ts
  • packages/lint/src/rules/actor-upload-media-property-required.ts
  • packages/lint/src/rules/media-uploader-object-uri-required.ts
  • packages/lint/src/tests/actor-upload-media-property-mismatch.test.ts
  • packages/lint/src/tests/actor-upload-media-property-required.test.ts
  • packages/lint/src/tests/media-uploader-object-uri-required.test.ts
  • packages/testing/src/context.ts
  • packages/testing/src/mock.ts
  • packages/vocab/src/endpoints.yaml
  • packages/vocab/src/vocab.test.ts

Comment thread packages/fedify/src/federation/handler.ts
Comment thread packages/fedify/src/federation/handler.ts
Comment thread packages/fedify/src/federation/middleware.test.ts
Comment thread packages/lint/src/index.ts
dahlia added a commit to dahlia/fedify-fork that referenced this pull request Jul 8, 2026
The media upload handler converted every other failure (multipart
parse, missing fields, callback throw) into a controlled response, but
two paths could still crash or leak an exception.

The callback's return type now permits a plain, non-Promise value, so a
JavaScript caller or a callback with no return statement can yield null
or undefined; reading result.id on such a value would throw and take
down the request.  It now returns a 500 instead of crashing.

Serializing the returned object with toJsonLd() was unguarded, so a
context-loader failure would escape out of Federation.fetch() (which
re-throws after logging) as an unhandled rejection rather than a clean
500 response.  It is now wrapped like every other failure mode in the
handler.

Also added a test for the plain-string object form field, whose branch
was previously never exercised because the test helper always sends the
field as a Blob.

fedify-dev#927 (comment)
fedify-dev#927 (comment)
fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
dahlia added a commit to dahlia/fedify-fork that referenced this pull request Jul 8, 2026
MediaUploaderCallback permits returning Object, URL, or a Promise of
either, so a synchronous callback may legitimately wrap its result in
Promise.resolve(...).  The media-uploader-object-uri-required rule only
recognized a direct getObjectUri() call, so such a callback was warned
about under the recommended lint configuration despite being valid.

unwrapExpression() now strips a single-argument Promise.resolve(...)
wrapper, alongside await and type assertions, before judging the
returned value, so both the URL and object-id forms are accepted
whether or not they are wrapped in a resolved promise.

fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
@dahlia

dahlia commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for the ActivityPub Media Upload extension, allowing servers to accept client-to-server media uploads. It introduces the setMediaUploader method to register an upload endpoint, the getMediaUploaderUri method to build the endpoint URI, and a new media_upload metric category. Additionally, three new lint rules are added to @fedify/lint to ensure proper configuration of the media upload endpoint. Feedback on the changes suggests enhancing the unwrapExpression helper in the media-uploader-object-uri-required lint rule to support TSTypeAssertion and ParenthesizedExpression nodes, preventing potential false positives.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/lint/src/rules/media-uploader-object-uri-required.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a5871f056

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/fedify/src/federation/handler.ts Outdated
dahlia added a commit to dahlia/fedify-fork that referenced this pull request Jul 8, 2026
unwrapExpression() in the media-uploader-object-uri-required rule
stripped the as, non-null, and satisfies assertions but not the
angle-bracket type assertion (<T>expr) nor a parenthesized expression,
so a returned value wrapped in either was not recognized as deriving
from getObjectUri() and produced a false positive.

fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
dahlia added a commit to dahlia/fedify-fork that referenced this pull request Jul 8, 2026
When an authorize hook was configured, the media upload handler cloned
the request to hand a copy to the hook.  Cloning a request tees its body
stream, so once the handler read the original with request.formData(),
the entire uploaded file was buffered a second time for the authorize
clone that a header/token hook never consumes; a large upload could
therefore double its peak memory footprint.

The hook now receives a header-only request built from the original's
URL, method, and headers, so the body is never teed.  On rejection the
original request (still unread, since formData() runs only after
authorization succeeds) is handed to onUnauthorized intact.

fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
@dahlia

dahlia commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for the ActivityPub Media Upload extension, enabling servers to accept client-to-server media uploads. It introduces setMediaUploader() to register the endpoint, getMediaUploaderUri() to retrieve its URI, and exposes the Endpoints.uploadMedia property. Additionally, three new lint rules are added to @fedify/lint to ensure correct configuration. Feedback on the changes suggests adding a defensive check in the request handler to ensure that the parsed JSON-LD object is not null before passing it to the callback, preventing potential runtime TypeErrors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/fedify/src/federation/handler.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc80c9d5cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/lint/src/rules/media-uploader-object-uri-required.ts Outdated
dahlia added a commit to dahlia/fedify-fork that referenced this pull request Jul 8, 2026
The media-uploader-object-uri-required rule accepted a bare object
literal return, such as () => ({ id: ctx.getObjectUri(...) }), as valid.
But the handler calls toJsonLd() on the result and returns a 500 when it
is missing, so a plain object is never a valid media uploader result.
The rule now only accepts a vocab object built with a constructor
(new X({...})) or a URL; a plain object literal falls through to the URL
check and is flagged.

fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
@dahlia

dahlia commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for the ActivityPub Media Upload extension, enabling servers to accept client-to-server media uploads. Key changes include adding the setMediaUploader() method to Federation and FederationBuilder to register a multipart/form-data upload endpoint, and adding getMediaUploaderUri() to Context to build the endpoint URI advertised under Endpoints.uploadMedia. Additionally, three new lint rules (media-uploader-object-uri-required, actor-upload-media-property-required, and actor-upload-media-property-mismatch) have been added to @fedify/lint to ensure correct usage. Comprehensive tests, documentation, and vocabulary updates have also been included. There are no review comments, and I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 17cde6a644

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sij411 sij411 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lint-rule edge case:

media-uploader-object-uri-required treats destructured getObjectUri calls as valid, e.g. async ({ getObjectUri }) => getObjectUri(...). However, the real ContextImpl. getObjectUri() method reads this.federation / this.canonicalOrigin, so destructuring the method loses its receiver and should throw at runtime. Unless these context methods are intentionally safe to call unbound, this rule may produce a falsse negative and the test case for destructured getObjectUri may be encouraging broken code. would it be safer to only accept ctx.getObjectUri(...) member calls on the callback's context parameter?

dahlia added a commit to dahlia/fedify-fork that referenced this pull request Jul 9, 2026
The media-uploader-object-uri-required rule accepted a getObjectUri call
destructured off the context parameter, as in
async ({ getObjectUri }) => getObjectUri(...).  But Context.getObjectUri()
reads this.federation and this.canonicalOrigin, so destructuring it loses
the method's receiver and throws a TypeError at runtime; accepting that
form let the rule pass provably broken code, and its test even encouraged
it.

The rule now recognizes only a member call on the callback's context
parameter (ctx.getObjectUri(...)); the destructured form is flagged, and
its test is inverted accordingly.  This also drops the alias tracking,
simplifying the rule.

fedify-dev#927 (review)

Assisted-by: Claude Code:claude-opus-4-8
@dahlia

dahlia commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@sij411 Good catch, thanks. I confirmed at runtime that destructuring getObjectUri off the context throws (TypeError: Cannot read properties of undefined (reading 'federation')), since the method reads this.federation/this.canonicalOrigin; the rule was passing provably broken code. Addressed in cb3ef37: the rule now accepts only ctx.getObjectUri(...) member calls on the callback's context parameter, and the destructured-alias test is inverted to expect a warning.

dahlia added 9 commits July 9, 2026 15:47
Fedify-based servers could accept client-to-server activities through
setOutboxListeners() (fedify-dev#430),
but had no way to accept the binary media uploads that the
client-to-server protocol relies on.  This adds support for the
ActivityPub Media Upload extension so applications can register a
dedicated multipart/form-data upload endpoint.

New API:

 -  Federation.setMediaUploader(path, callback) registers the endpoint;
    the callback finalizes the uploaded file and returns either the
    created object (201 Created) or the URL at which it will become
    available (202 Accepted).  The returned setter exposes .authorize().
 -  Context.getMediaUploaderUri(identifier) builds the endpoint URI,
    which actor dispatchers advertise under the new
    Endpoints.uploadMedia vocabulary property.

The endpoint mirrors the outbox path: it rejects uploads for missing or
tombstoned actors before invoking the callback, requires
multipart/form-data, and returns 201/202 with a Location header (an
object without an id yields 500, since a 201 cannot omit Location).  It
warns at runtime when a returned id/URL does not point at a registered
object dispatcher route, or when a registered uploader is not advertised
under endpoints.uploadMedia.

@fedify/lint gains three rules:

 -  media-uploader-object-uri-required
 -  actor-upload-media-property-required
 -  actor-upload-media-property-mismatch

See fedify-dev#754 for the design
discussion.

Assisted-by: Claude Code:claude-opus-4-8
Assisted-by: Codex:gpt-5.5
The media upload handler converted every other failure (multipart
parse, missing fields, callback throw) into a controlled response, but
two paths could still crash or leak an exception.

The callback's return type now permits a plain, non-Promise value, so a
JavaScript caller or a callback with no return statement can yield null
or undefined; reading result.id on such a value would throw and take
down the request.  It now returns a 500 instead of crashing.

Serializing the returned object with toJsonLd() was unguarded, so a
context-loader failure would escape out of Federation.fetch() (which
re-throws after logging) as an unhandled rejection rather than a clean
500 response.  It is now wrapped like every other failure mode in the
handler.

Also added a test for the plain-string object form field, whose branch
was previously never exercised because the test helper always sends the
field as a Blob.

fedify-dev#927 (comment)
fedify-dev#927 (comment)
fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
MediaUploaderCallback permits returning Object, URL, or a Promise of
either, so a synchronous callback may legitimately wrap its result in
Promise.resolve(...).  The media-uploader-object-uri-required rule only
recognized a direct getObjectUri() call, so such a callback was warned
about under the recommended lint configuration despite being valid.

unwrapExpression() now strips a single-argument Promise.resolve(...)
wrapper, alongside await and type assertions, before judging the
returned value, so both the URL and object-id forms are accepted
whether or not they are wrapped in a resolved promise.

fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
unwrapExpression() in the media-uploader-object-uri-required rule
stripped the as, non-null, and satisfies assertions but not the
angle-bracket type assertion (<T>expr) nor a parenthesized expression,
so a returned value wrapped in either was not recognized as deriving
from getObjectUri() and produced a false positive.

fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
When an authorize hook was configured, the media upload handler cloned
the request to hand a copy to the hook.  Cloning a request tees its body
stream, so once the handler read the original with request.formData(),
the entire uploaded file was buffered a second time for the authorize
clone that a header/token hook never consumes; a large upload could
therefore double its peak memory footprint.

The hook now receives a header-only request built from the original's
URL, method, and headers, so the body is never teed.  On rejection the
original request (still unread, since formData() runs only after
authorization succeeds) is handed to onUnauthorized intact.

fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
The media-uploader-object-uri-required rule accepted a bare object
literal return, such as () => ({ id: ctx.getObjectUri(...) }), as valid.
But the handler calls toJsonLd() on the result and returns a 500 when it
is missing, so a plain object is never a valid media uploader result.
The rule now only accepts a vocab object built with a constructor
(new X({...})) or a URL; a plain object literal falls through to the URL
check and is flagged.

fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
The previous change gave the authorize hook a header-only request to
avoid teeing the upload body, but that broke signature-based hooks: a
hook verifying an RFC 9421 Content-Digest would hash an empty body
against the multipart digest header and reject otherwise valid signed
uploads.

The handler now reads the body once into memory (only when an authorize
hook is configured) and rebuilds a fresh Request from those bytes for
the hook, for onUnauthorized, and for request.formData().  A
signature-verifying hook therefore sees the real body and headers, while
the file is still buffered only once rather than teed and buffered a
second time for the common token/header hook.

fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
Two issues in the media upload authorization path:

The actor was resolved after the authorize hook, so an authorize hook
that rejects a request for a nonexistent or tombstoned actor returned
401 instead of the documented 404, and the whole multipart body was
buffered for a bogus identifier before the cheap actor-existence check.
The actor is now resolved first, so a missing actor gets 404 without
buffering the body or running authorization.

The pre-authorization body read (request.arrayBuffer()) was unguarded,
so an aborted or errored upload rejected and escaped handleMediaUpload
as an unhandled server error instead of a controlled response.  It is
now wrapped like request.formData() and converted into a 400.

fedify-dev#927 (comment)
fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
The media-uploader-object-uri-required rule accepted a getObjectUri call
destructured off the context parameter, as in
async ({ getObjectUri }) => getObjectUri(...).  But Context.getObjectUri()
reads this.federation and this.canonicalOrigin, so destructuring it loses
the method's receiver and throws a TypeError at runtime; accepting that
form let the rule pass provably broken code, and its test even encouraged
it.

The rule now recognizes only a member call on the callback's context
parameter (ctx.getObjectUri(...)); the destructured form is flagged, and
its test is inverted accordingly.  This also drops the alias tracking,
simplifying the rule.

fedify-dev#927 (review)

Assisted-by: Claude Code:claude-opus-4-8
@dahlia dahlia force-pushed the media-upload-ext branch from cb3ef37 to 9b57611 Compare July 9, 2026 07:00
@dahlia dahlia requested a review from sij411 July 9, 2026 07:18
Comment thread packages/fedify/src/federation/handler.ts
Comment thread docs/manual/media-upload.md
dahlia added 2 commits July 9, 2026 17:01
The media upload handler resolves the actor before authorization, which
differs from the outbox (which authorizes first) and means a request for
a nonexistent or deleted actor gets 404 rather than 401 even when it
would also fail authorization.  This is deliberate: it avoids buffering
the upload for a bogus identifier and keeps the 404 reliable regardless
of the authorize hook, and it leaks nothing new since actor existence is
already public via the actor URI.

Document the ordering and its consequence in the media upload guide, and
clarify the code comment, instead of implying the two handlers share an
ordering.  The PR description is corrected separately.

fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
A media uploader registered without .authorize() accepts uploads from
anyone who can reach the URL, which for an endpoint that stores files is
a serious abuse, denial-of-service, and storage-cost risk.  Make that
hard to miss:

 -  The media upload guide now carries a prominent warning that
    .authorize() must be configured in production unless a public upload
    endpoint is intended.
 -  Fedify logs a one-time runtime warning when a media uploader handles
    a request without an authorize predicate; the mediaUploader log
    category is now documented in the logging guide.
 -  A new @fedify/lint rule, media-uploader-authorization-required, flags
    a setMediaUploader() call that is not protected with .authorize().

fedify-dev#927 (comment)

Assisted-by: Claude Code:claude-opus-4-8
@dahlia

dahlia commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@dahlia dahlia requested a review from 2chanhaeng July 9, 2026 08:13
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 8fdd9a06a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@dahlia dahlia merged commit 5885824 into fedify-dev:main Jul 9, 2026
20 of 21 checks passed
@dahlia dahlia deleted the media-upload-ext branch July 9, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

activitypub/compliance Specification compliance component/federation Federation object related component/lint Lint related (@fedify/lint)

Development

Successfully merging this pull request may close these issues.

Support ActivityPub Media Upload extension via setMediaUploader()

3 participants