33import {
44 type Context ,
55 createFederation ,
6+ type Federation ,
67 generateCryptoKeyPair ,
78 MemoryKvStore ,
89 verifyRequest ,
910} from "@fedify/fedify" ;
11+ import type { DocumentLoader } from "@fedify/vocab-runtime" ;
1012import {
1113 Accept ,
1214 Activity ,
@@ -110,7 +112,7 @@ export const inboxCommand = command(
110112 "--authorized-fetch" ,
111113 {
112114 description :
113- message `Enable authorized fetch mode. Incoming requests without valid HTTP signatures with 401 Unauthorized will be rejected.` ,
115+ message `Enable authorized fetch mode. Incoming requests without valid HTTP signatures will be rejected with 401 Unauthorized .` ,
114116 } ,
115117 ) ,
116118 } ) ,
@@ -123,23 +125,177 @@ export const inboxCommand = command(
123125 } ,
124126) ;
125127
128+ // Module-level state
129+ const activities : ActivityEntry [ ] = [ ] ;
130+ const acceptFollows : string [ ] = [ ] ;
131+ const peers : Record < string , Actor > = { } ;
132+ const followers : Record < string , Actor > = { } ;
133+
126134export async function runInbox (
127135 command : InferValue < typeof inboxCommand > ,
128136) {
129- const fetch = createFetchHandler ( {
130- actorName : command . actorName ,
131- actorSummary : command . actorSummary ,
132- } , command . authorizedFetch ) ;
133- const sendDeleteToPeers = createSendDeleteToPeers ( {
134- actorName : command . actorName ,
135- actorSummary : command . actorSummary ,
136- } ) ;
137-
138137 // Enable Debug mode if requested
139138 if ( command . debug ) {
140139 await configureLogging ( ) ;
141140 }
142141
142+ // Create federation inside runInbox to configure skipSignatureVerification
143+ const federationDocumentLoader = await getDocumentLoader ( ) ;
144+ const authorizedFetchEnabled = command . authorizedFetch ?? false ;
145+
146+ const federation = createFederation < ContextData > ( {
147+ kv : new MemoryKvStore ( ) ,
148+ documentLoaderFactory : ( ) => federationDocumentLoader ,
149+ // When authorizedFetch is enabled, we verify all requests manually,
150+ // so skip federation's inbox signature verification to avoid double verification
151+ skipSignatureVerification : authorizedFetchEnabled ,
152+ } ) ;
153+
154+ const time = Temporal . Now . instant ( ) ;
155+ let actorKeyPairs : CryptoKeyPair [ ] | undefined = undefined ;
156+
157+ // Set up actor dispatcher
158+ federation
159+ . setActorDispatcher ( "/{identifier}" , async ( ctx , identifier ) => {
160+ if ( identifier !== "i" ) return null ;
161+ return new Application ( {
162+ id : ctx . getActorUri ( identifier ) ,
163+ preferredUsername : identifier ,
164+ name : ctx . data . actorName ,
165+ summary : ctx . data . actorSummary ,
166+ inbox : ctx . getInboxUri ( identifier ) ,
167+ endpoints : new Endpoints ( {
168+ sharedInbox : ctx . getInboxUri ( ) ,
169+ } ) ,
170+ followers : ctx . getFollowersUri ( identifier ) ,
171+ following : ctx . getFollowingUri ( identifier ) ,
172+ outbox : ctx . getOutboxUri ( identifier ) ,
173+ manuallyApprovesFollowers : true ,
174+ published : time ,
175+ icon : new Image ( {
176+ url : new URL ( "https://fedify.dev/logo.png" ) ,
177+ mediaType : "image/png" ,
178+ } ) ,
179+ publicKey : ( await ctx . getActorKeyPairs ( identifier ) ) [ 0 ] . cryptographicKey ,
180+ assertionMethods : ( await ctx . getActorKeyPairs ( identifier ) )
181+ . map ( ( pair ) => pair . multikey ) ,
182+ url : ctx . getActorUri ( identifier ) ,
183+ } ) ;
184+ } )
185+ . setKeyPairsDispatcher ( async ( _ctxData , identifier ) => {
186+ if ( identifier !== "i" ) return [ ] ;
187+ if ( actorKeyPairs == null ) {
188+ actorKeyPairs = [
189+ await generateCryptoKeyPair ( "RSASSA-PKCS1-v1_5" ) ,
190+ await generateCryptoKeyPair ( "Ed25519" ) ,
191+ ] ;
192+ }
193+ return actorKeyPairs ;
194+ } ) ;
195+
196+ // Set up inbox listeners
197+ federation
198+ . setInboxListeners ( "/{identifier}/inbox" , "/inbox" )
199+ . setSharedKeyDispatcher ( ( _ ) => ( { identifier : "i" } ) )
200+ . on ( Activity , async ( ctx , activity ) => {
201+ activities [ ctx . data . activityIndex ] . activity = activity ;
202+ for await ( const actor of activity . getActors ( ) ) {
203+ if ( actor . id != null ) peers [ actor . id . href ] = actor ;
204+ }
205+ for await ( const actor of activity . getAttributions ( ) ) {
206+ if ( actor . id != null ) peers [ actor . id . href ] = actor ;
207+ }
208+ if ( activity instanceof Follow ) {
209+ if ( acceptFollows . length < 1 ) return ;
210+ const objectId = activity . objectId ;
211+ if ( objectId == null ) return ;
212+ const parsed = ctx . parseUri ( objectId ) ;
213+ if ( parsed ?. type !== "actor" || parsed . identifier !== "i" ) return ;
214+ const { identifier } = parsed ;
215+ const follower = await activity . getActor ( ) ;
216+ if ( ! isActor ( follower ) ) return ;
217+ const accepts = await matchesActor ( follower , acceptFollows ) ;
218+ if ( ! accepts || activity . id == null ) {
219+ logger . debug ( "Does not accept follow from {actor}." , {
220+ actor : follower . id ?. href ,
221+ } ) ;
222+ return ;
223+ }
224+ logger . debug ( "Accepting follow from {actor}." , {
225+ actor : follower . id ?. href ,
226+ } ) ;
227+ followers [ activity . id . href ] = follower ;
228+ await ctx . sendActivity (
229+ { identifier } ,
230+ follower ,
231+ new Accept ( {
232+ id : new URL ( `#accepts/${ follower . id ?. href } ` , ctx . getActorUri ( "i" ) ) ,
233+ actor : ctx . getActorUri ( identifier ) ,
234+ object : activity . id ,
235+ } ) ,
236+ ) ;
237+ }
238+ } ) ;
239+
240+ // Set up collection dispatchers
241+ federation
242+ . setFollowersDispatcher ( "/{identifier}/followers" , ( _ctx , identifier ) => {
243+ if ( identifier !== "i" ) return null ;
244+ const items : Recipient [ ] = [ ] ;
245+ for ( const follower of Object . values ( followers ) ) {
246+ if ( follower . id == null ) continue ;
247+ items . push ( follower ) ;
248+ }
249+ return { items } ;
250+ } )
251+ . setCounter ( ( _ctx , identifier ) => {
252+ if ( identifier !== "i" ) return null ;
253+ return Object . keys ( followers ) . length ;
254+ } ) ;
255+
256+ federation
257+ . setFollowingDispatcher (
258+ "/{identifier}/following" ,
259+ ( _ctx , _identifier ) => null ,
260+ )
261+ . setCounter ( ( _ctx , _identifier ) => 0 ) ;
262+
263+ federation
264+ . setOutboxDispatcher ( "/{identifier}/outbox" , ( _ctx , _identifier ) => null )
265+ . setCounter ( ( _ctx , _identifier ) => 0 ) ;
266+
267+ federation . setNodeInfoDispatcher ( "/nodeinfo/2.1" , ( _ctx ) => {
268+ return {
269+ software : {
270+ name : "fedify-cli" ,
271+ version : metadata . version ,
272+ repository : new URL ( "https://github.com/fedify-dev/fedify" ) ,
273+ } ,
274+ protocols : [ "activitypub" ] ,
275+ usage : {
276+ users : {
277+ total : 1 ,
278+ activeMonth : 1 ,
279+ activeHalfyear : 1 ,
280+ } ,
281+ localComments : 0 ,
282+ localPosts : 0 ,
283+ } ,
284+ } ;
285+ } ) ;
286+
287+ // Create handlers with the configured federation
288+ const fetch = createFetchHandler (
289+ federation ,
290+ federationDocumentLoader ,
291+ { actorName : command . actorName , actorSummary : command . actorSummary } ,
292+ authorizedFetchEnabled ,
293+ ) ;
294+ const sendDeleteToPeers = createSendDeleteToPeers (
295+ federation ,
296+ { actorName : command . actorName , actorSummary : command . actorSummary } ,
297+ ) ;
298+
143299 const spinner = ora ( {
144300 text : "Spinning up an ephemeral ActivityPub server..." ,
145301 discardStdin : false ,
@@ -212,63 +368,8 @@ export async function runInbox(
212368 printServerInfo ( fedCtx ) ;
213369}
214370
215- const federationDocumentLoader = await getDocumentLoader ( ) ;
216-
217- const federation = createFederation < ContextData > ( {
218- kv : new MemoryKvStore ( ) ,
219- documentLoaderFactory : ( ) => {
220- return federationDocumentLoader ;
221- } ,
222- } ) ;
223-
224- const time = Temporal . Now . instant ( ) ;
225- let actorKeyPairs : CryptoKeyPair [ ] | undefined = undefined ;
226-
227- federation
228- . setActorDispatcher ( "/{identifier}" , async ( ctx , identifier ) => {
229- if ( identifier !== "i" ) return null ;
230- return new Application ( {
231- id : ctx . getActorUri ( identifier ) ,
232- preferredUsername : identifier ,
233- name : ctx . data . actorName ,
234- summary : ctx . data . actorSummary ,
235- inbox : ctx . getInboxUri ( identifier ) ,
236- endpoints : new Endpoints ( {
237- sharedInbox : ctx . getInboxUri ( ) ,
238- } ) ,
239- followers : ctx . getFollowersUri ( identifier ) ,
240- following : ctx . getFollowingUri ( identifier ) ,
241- outbox : ctx . getOutboxUri ( identifier ) ,
242- manuallyApprovesFollowers : true ,
243- published : time ,
244- icon : new Image ( {
245- url : new URL ( "https://fedify.dev/logo.png" ) ,
246- mediaType : "image/png" ,
247- } ) ,
248- publicKey : ( await ctx . getActorKeyPairs ( identifier ) ) [ 0 ] . cryptographicKey ,
249- assertionMethods : ( await ctx . getActorKeyPairs ( identifier ) )
250- . map ( ( pair ) => pair . multikey ) ,
251- url : ctx . getActorUri ( identifier ) ,
252- } ) ;
253- } )
254- . setKeyPairsDispatcher ( async ( _ctxData , identifier ) => {
255- if ( identifier !== "i" ) return [ ] ;
256- if ( actorKeyPairs == null ) {
257- actorKeyPairs = [
258- await generateCryptoKeyPair ( "RSASSA-PKCS1-v1_5" ) ,
259- await generateCryptoKeyPair ( "Ed25519" ) ,
260- ] ;
261- }
262- return actorKeyPairs ;
263- } ) ;
264-
265- const activities : ActivityEntry [ ] = [ ] ;
266-
267- const acceptFollows : string [ ] = [ ] ;
268-
269- const peers : Record < string , Actor > = { } ;
270-
271371function createSendDeleteToPeers (
372+ federation : Federation < ContextData > ,
272373 actorOptions : { actorName : string ; actorSummary : string } ,
273374) : ( server : TemporaryServer ) => Promise < void > {
274375 return async function sendDeleteToPeers (
@@ -301,97 +402,6 @@ function createSendDeleteToPeers(
301402 } ;
302403}
303404
304- const followers : Record < string , Actor > = { } ;
305-
306- federation
307- . setInboxListeners ( "/{identifier}/inbox" , "/inbox" )
308- . setSharedKeyDispatcher ( ( _ ) => ( { identifier : "i" } ) )
309- . on ( Activity , async ( ctx , activity ) => {
310- activities [ ctx . data . activityIndex ] . activity = activity ;
311- for await ( const actor of activity . getActors ( ) ) {
312- if ( actor . id != null ) peers [ actor . id . href ] = actor ;
313- }
314- for await ( const actor of activity . getAttributions ( ) ) {
315- if ( actor . id != null ) peers [ actor . id . href ] = actor ;
316- }
317- if ( activity instanceof Follow ) {
318- if ( acceptFollows . length < 1 ) return ;
319- const objectId = activity . objectId ;
320- if ( objectId == null ) return ;
321- const parsed = ctx . parseUri ( objectId ) ;
322- if ( parsed ?. type !== "actor" || parsed . identifier !== "i" ) return ;
323- const { identifier } = parsed ;
324- const follower = await activity . getActor ( ) ;
325- if ( ! isActor ( follower ) ) return ;
326- const accepts = await matchesActor ( follower , acceptFollows ) ;
327- if ( ! accepts || activity . id == null ) {
328- logger . debug ( "Does not accept follow from {actor}." , {
329- actor : follower . id ?. href ,
330- } ) ;
331- return ;
332- }
333- logger . debug ( "Accepting follow from {actor}." , {
334- actor : follower . id ?. href ,
335- } ) ;
336- followers [ activity . id . href ] = follower ;
337- await ctx . sendActivity (
338- { identifier } ,
339- follower ,
340- new Accept ( {
341- id : new URL ( `#accepts/${ follower . id ?. href } ` , ctx . getActorUri ( "i" ) ) ,
342- actor : ctx . getActorUri ( identifier ) ,
343- object : activity . id ,
344- } ) ,
345- ) ;
346- }
347- } ) ;
348-
349- federation
350- . setFollowersDispatcher ( "/{identifier}/followers" , ( _ctx , identifier ) => {
351- if ( identifier !== "i" ) return null ;
352- const items : Recipient [ ] = [ ] ;
353- for ( const follower of Object . values ( followers ) ) {
354- if ( follower . id == null ) continue ;
355- items . push ( follower ) ;
356- }
357- return { items } ;
358- } )
359- . setCounter ( ( _ctx , identifier ) => {
360- if ( identifier !== "i" ) return null ;
361- return Object . keys ( followers ) . length ;
362- } ) ;
363-
364- federation
365- . setFollowingDispatcher (
366- "/{identifier}/following" ,
367- ( _ctx , _identifier ) => null ,
368- )
369- . setCounter ( ( _ctx , _identifier ) => 0 ) ;
370-
371- federation
372- . setOutboxDispatcher ( "/{identifier}/outbox" , ( _ctx , _identifier ) => null )
373- . setCounter ( ( _ctx , _identifier ) => 0 ) ;
374-
375- federation . setNodeInfoDispatcher ( "/nodeinfo/2.1" , ( _ctx ) => {
376- return {
377- software : {
378- name : "fedify-cli" ,
379- version : metadata . version ,
380- repository : new URL ( "https://github.com/fedify-dev/fedify" ) ,
381- } ,
382- protocols : [ "activitypub" ] ,
383- usage : {
384- users : {
385- total : 1 ,
386- activeMonth : 1 ,
387- activeHalfyear : 1 ,
388- } ,
389- localComments : 0 ,
390- localPosts : 0 ,
391- } ,
392- } ;
393- } ) ;
394-
395405function printServerInfo ( fedCtx : Context < ContextData > ) : void {
396406 const table = new Table ( {
397407 chars : tableStyle ,
@@ -493,6 +503,8 @@ app.get("/r/:idx{[0-9]+}", (c) => {
493503} ) ;
494504
495505function createFetchHandler (
506+ federation : Federation < ContextData > ,
507+ documentLoader : DocumentLoader ,
496508 actorOptions : { actorName : string ; actorSummary : string } ,
497509 authorizedFetchEnabled : boolean ,
498510) : ( request : Request ) => Promise < Response > {
@@ -505,9 +517,7 @@ function createFetchHandler(
505517 }
506518
507519 if ( authorizedFetchEnabled ) {
508- const key = await verifyRequest ( request , {
509- documentLoader : federationDocumentLoader ,
510- } ) ;
520+ const key = await verifyRequest ( request , { documentLoader } ) ;
511521 if ( key == null ) {
512522 logger . error (
513523 "Unauthorized request: HTTP Signature verification failed for {method} {path}" ,
0 commit comments