@@ -756,6 +756,87 @@ federation
756756 });
757757~~~~
758758
759+ ### ` media-uploader-object-uri-required `
760+
761+ Warns when a ` setMediaUploader() ` callback returns a value that is not derived
762+ from ` ctx.getObjectUri() ` .
763+
764+ ** When this rule applies:**
765+ You've registered a media uploader with ` setMediaUploader() ` , but its callback
766+ never references ` ctx.getObjectUri() ` , so the returned ` id ` /` URL ` probably does
767+ not point at a registered object dispatcher route.
768+
769+ ** Why it matters:**
770+ The upload endpoint emits object IDs, but serving those IDs back as fetchable
771+ ActivityStreams objects is the developer's job via ` setObjectDispatcher() ` .
772+ Deriving the returned value from ` ctx.getObjectUri() ` keeps the emitted ID in
773+ sync with a registered object dispatcher route.
774+
775+ ~~~~ typescript twoslash
776+ // @noErrors: 2345
777+ import { createFederation } from " @fedify/fedify" ;
778+ import { Image } from " @fedify/vocab" ;
779+ const federation = createFederation <void >({ kv: null as any });
780+ // ---cut-before---
781+ // ❌ Bad: Returned id is a hard-coded URL, not from ctx.getObjectUri()
782+ federation .setMediaUploader (
783+ " /users/{identifier}/media" ,
784+ async (ctx , identifier , file , object ) => {
785+ return new Image ({ id: new URL (" https://example.com/media/1" ) });
786+ },
787+ );
788+
789+ // ✅ Good: Returned id is derived from ctx.getObjectUri()
790+ federation .setMediaUploader (
791+ " /users/{identifier}/media" ,
792+ async (ctx , identifier , file , object ) => {
793+ return new Image ({
794+ id: ctx .getObjectUri (Image , { uuid: " 1" }),
795+ mediaType: file .type ,
796+ });
797+ },
798+ );
799+ ~~~~
800+
801+ ### ` media-uploader-authorization-required `
802+
803+ Warns when ` setMediaUploader() ` is registered without an ` .authorize() ` hook.
804+
805+ ** When this rule applies:**
806+ You've called ` federation.setMediaUploader() ` but never chained (or otherwise
807+ called) ` .authorize() ` on the returned setter.
808+
809+ ** Why it matters:**
810+ Without an authorize hook, the upload endpoint accepts uploads from anyone who
811+ can reach the URL. For an endpoint that stores files, that is a serious abuse,
812+ denial-of-service, and storage-cost risk, so ` .authorize() ` should be
813+ configured unless a public upload endpoint is genuinely intended.
814+
815+ ~~~~ typescript twoslash
816+ // @noErrors: 2345
817+ import { createFederation } from " @fedify/fedify" ;
818+ import { Image } from " @fedify/vocab" ;
819+ const federation = createFederation <void >({ kv: null as any });
820+ // ---cut-before---
821+ // ❌ Bad: No authorize() hook, so anyone can upload
822+ federation .setMediaUploader (
823+ " /users/{identifier}/media" ,
824+ async (ctx , identifier , file , object ) =>
825+ ctx .getObjectUri (Image , { uuid: " 1" }),
826+ );
827+
828+ // ✅ Good: Protected with authorize()
829+ federation
830+ .setMediaUploader (
831+ " /users/{identifier}/media" ,
832+ async (ctx , identifier , file , object ) =>
833+ ctx .getObjectUri (Image , { uuid: " 1" }),
834+ )
835+ .authorize (async (ctx , identifier ) => {
836+ return ctx .request .headers .get (" authorization" ) === ` Bearer ${identifier } ` ;
837+ });
838+ ~~~~
839+
759840### ` actor-followers-property-required `
760841
761842Ensures ` followers ` is defined when ` setFollowersDispatcher() ` is configured.
@@ -1219,6 +1300,92 @@ federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => {
12191300});
12201301~~~~
12211302
1303+ ### ` actor-upload-media-property-required `
1304+
1305+ Ensures ` endpoints.uploadMedia ` is defined when ` setMediaUploader() ` is
1306+ configured.
1307+
1308+ ** When this rule applies:**
1309+ You've called ` federation.setMediaUploader() ` , but the actor object doesn't
1310+ advertise the endpoint under an
1311+ ` endpoints: new Endpoints({ uploadMedia: ... }) ` property.
1312+
1313+ ** Why it matters:**
1314+ Registering a media uploader does not by itself expose the endpoint to clients.
1315+ Advertising it under ` endpoints.uploadMedia ` is what lets clients discover where
1316+ to upload media.
1317+
1318+ ~~~~ typescript twoslash
1319+ // @noErrors: 2345
1320+ import { createFederation } from " @fedify/fedify" ;
1321+ import { Endpoints , Person } from " @fedify/vocab" ;
1322+ const federation = createFederation <void >({ kv: null as any });
1323+ // ---cut-before---
1324+ // ❌ Bad: Missing endpoints.uploadMedia when a media uploader is registered
1325+ federation .setActorDispatcher (" /users/{identifier}" , (ctx , identifier ) => {
1326+ return new Person ({
1327+ id: ctx .getActorUri (identifier ),
1328+ // Missing endpoints.uploadMedia!
1329+ });
1330+ });
1331+
1332+ federation .setMediaUploader (
1333+ " /users/{identifier}/media" ,
1334+ async (ctx , identifier , file , object ) =>
1335+ ctx .getObjectUri (Person , { uuid: " 1" }),
1336+ );
1337+
1338+ // ✅ Good: Advertise endpoints.uploadMedia
1339+ federation .setActorDispatcher (" /users/{identifier}" , (ctx , identifier ) => {
1340+ return new Person ({
1341+ id: ctx .getActorUri (identifier ),
1342+ endpoints: new Endpoints ({
1343+ uploadMedia: ctx .getMediaUploaderUri (identifier ),
1344+ }),
1345+ });
1346+ });
1347+ ~~~~
1348+
1349+ ### ` actor-upload-media-property-mismatch `
1350+
1351+ Validates that ` endpoints.uploadMedia ` is set using
1352+ ` ctx.getMediaUploaderUri(identifier) ` .
1353+
1354+ ** When this rule applies:**
1355+ The ` endpoints.uploadMedia ` property is set to a value other than
1356+ ` ctx.getMediaUploaderUri(identifier) ` .
1357+
1358+ ** Why it matters:**
1359+ The advertised upload endpoint URI must match the path configured in
1360+ ` setMediaUploader() ` , or clients will upload to the wrong URL.
1361+
1362+ ~~~~ typescript twoslash
1363+ // @noErrors: 2345
1364+ import { createFederation } from " @fedify/fedify" ;
1365+ import { Endpoints , Person } from " @fedify/vocab" ;
1366+ const federation = createFederation <void >({ kv: null as any });
1367+ // ---cut-before---
1368+ // ❌ Bad: Using a hard-coded URL for the upload endpoint
1369+ federation .setActorDispatcher (" /users/{identifier}" , (ctx , identifier ) => {
1370+ return new Person ({
1371+ id: ctx .getActorUri (identifier ),
1372+ endpoints: new Endpoints ({
1373+ uploadMedia: new URL (" https://example.com/upload" ), // Wrong!
1374+ }),
1375+ });
1376+ });
1377+
1378+ // ✅ Good: Use ctx.getMediaUploaderUri(identifier)
1379+ federation .setActorDispatcher (" /users/{identifier}" , (ctx , identifier ) => {
1380+ return new Person ({
1381+ id: ctx .getActorUri (identifier ),
1382+ endpoints: new Endpoints ({
1383+ uploadMedia: ctx .getMediaUploaderUri (identifier ),
1384+ }),
1385+ });
1386+ });
1387+ ~~~~
1388+
12221389### ` collection-filtering-not-implemented `
12231390
12241391Warns when collection dispatchers don't implement filtering.
0 commit comments