Loading CHANGES.md +2 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,8 @@ Version 1.2.0 To be released. - Added `InboxContext.recipient` property. - Added NodeInfo client functions. - Added `getNodeInfo()` function. Loading src/federation/context.ts +7 −0 Original line number Diff line number Diff line Loading @@ -384,6 +384,13 @@ export interface RequestContext<TContextData> extends Context<TContextData> { * @since 1.0.0 */ export interface InboxContext<TContextData> extends Context<TContextData> { /** * The identifier of the recipient of the inbox. If the inbox is a shared * inbox, it is `null`. * @since 1.2.0 */ recipient: string | null; /** * Forwards a received activity to the recipients' inboxes. The forwarded * activity will be signed in HTTP Signatures by the forwarder, but its Loading src/federation/handler.test.ts +12 −12 Original line number Diff line number Diff line Loading @@ -1099,7 +1099,7 @@ test("handleInbox()", async () => { skipSignatureVerification: false, } as const; let response = await handleInbox(unsignedRequest, { identifier: null, recipient: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); Loading @@ -1112,10 +1112,10 @@ test("handleInbox()", async () => { onNotFoundCalled = null; response = await handleInbox(unsignedRequest, { identifier: "nobody", recipient: "nobody", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); return createInboxContext({ ...unsignedContext, recipient: "nobody" }); }, ...inboxOptions, }); Loading @@ -1124,7 +1124,7 @@ test("handleInbox()", async () => { onNotFoundCalled = null; response = await handleInbox(unsignedRequest, { identifier: null, recipient: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); Loading @@ -1135,10 +1135,10 @@ test("handleInbox()", async () => { assertEquals(response.status, 401); response = await handleInbox(unsignedRequest, { identifier: "someone", recipient: "someone", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); return createInboxContext({ ...unsignedContext, recipient: "someone" }); }, ...inboxOptions, }); Loading @@ -1158,7 +1158,7 @@ test("handleInbox()", async () => { documentLoader: mockDocumentLoader, }); response = await handleInbox(signedRequest, { identifier: null, recipient: null, context: signedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); Loading @@ -1169,10 +1169,10 @@ test("handleInbox()", async () => { assertEquals(response.status, 202); response = await handleInbox(signedRequest, { identifier: "someone", recipient: "someone", context: signedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); return createInboxContext({ ...unsignedContext, recipient: "someone" }); }, ...inboxOptions, }); Loading @@ -1180,7 +1180,7 @@ test("handleInbox()", async () => { assertEquals(response.status, 202); response = await handleInbox(unsignedRequest, { identifier: null, recipient: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); Loading @@ -1192,10 +1192,10 @@ test("handleInbox()", async () => { assertEquals(response.status, 202); response = await handleInbox(unsignedRequest, { identifier: "someone", recipient: "someone", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); return createInboxContext({ ...unsignedContext, recipient: "someone" }); }, ...inboxOptions, skipSignatureVerification: true, Loading src/federation/handler.ts +37 −24 Original line number Diff line number Diff line Loading @@ -341,9 +341,10 @@ function filterCollectionItems<TItem extends Object | Link | Recipient | URL>( } export interface InboxHandlerParameters<TContextData> { identifier: string | null; recipient: string | null; context: RequestContext<TContextData>; inboxContextFactory( recipient: string | null, activity: unknown, ): InboxContext<TContextData>; kv: KvStore; Loading @@ -363,7 +364,7 @@ export interface InboxHandlerParameters<TContextData> { export async function handleInbox<TContextData>( request: Request, { identifier, recipient, context, inboxContextFactory, kv, Loading @@ -379,12 +380,12 @@ export async function handleInbox<TContextData>( ): Promise<Response> { const logger = getLogger(["fedify", "federation", "inbox"]); if (actorDispatcher == null) { logger.error("Actor dispatcher is not set.", { identifier }); logger.error("Actor dispatcher is not set.", { recipient }); return await onNotFound(request); } else if (identifier != null) { const actor = await actorDispatcher(context, identifier); } else if (recipient != null) { const actor = await actorDispatcher(context, recipient); if (actor == null) { logger.error("Actor {identifier} not found.", { identifier }); logger.error("Actor {recipient} not found.", { recipient }); return await onNotFound(request); } } Loading @@ -392,13 +393,13 @@ export async function handleInbox<TContextData>( try { json = await request.clone().json(); } catch (error) { logger.error("Failed to parse JSON:\n{error}", { identifier, error }); logger.error("Failed to parse JSON:\n{error}", { recipient, error }); try { await inboxErrorHandler?.(context, error as Error); } catch (error) { logger.error( "An unexpected error occurred in inbox error handler:\n{error}", { error, activity: json }, { error, activity: json, recipient }, ); } return new Response("Invalid JSON.", { Loading Loading @@ -437,12 +438,12 @@ export async function handleInbox<TContextData>( const jsonWithoutSig = detachSignature(json); let activity: Activity | null = null; if (ldSigVerified) { logger.debug("Linked Data Signatures are verified.", { identifier, json }); logger.debug("Linked Data Signatures are verified.", { recipient, json }); activity = await Activity.fromJsonLd(jsonWithoutSig, context); } else { logger.debug( "Linked Data Signatures are not verified.", { identifier, json }, { recipient, json }, ); try { activity = await verifyObject(Activity, jsonWithoutSig, { Loading @@ -452,8 +453,8 @@ export async function handleInbox<TContextData>( }); } catch (error) { logger.error("Failed to parse activity:\n{error}", { identifier, json, recipient, activity: json, error, }); try { Loading @@ -461,7 +462,7 @@ export async function handleInbox<TContextData>( } catch (error) { logger.error( "An unexpected error occurred in inbox error handler:\n{error}", { error, activity: json }, { error, activity: json, recipient }, ); } return new Response("Invalid activity.", { Loading @@ -472,12 +473,12 @@ export async function handleInbox<TContextData>( if (activity == null) { logger.debug( "Object Integrity Proofs are not verified.", { identifier, json }, { recipient, activity: json }, ); } else { logger.debug( "Object Integrity Proofs are verified.", { identifier, json }, { recipient, activity: json }, ); } } Loading @@ -493,7 +494,7 @@ export async function handleInbox<TContextData>( if (key == null) { logger.error( "Failed to verify the request's HTTP Signatures.", { identifier }, { recipient }, ); const response = new Response( "Failed to verify the request signature.", Loading @@ -504,7 +505,7 @@ export async function handleInbox<TContextData>( ); return response; } else { logger.debug("HTTP Signatures are verified.", { identifier }); logger.debug("HTTP Signatures are verified.", { recipient }); } httpSigKey = key; } Loading @@ -519,6 +520,7 @@ export async function handleInbox<TContextData>( logger.debug("Activity {activityId} has already been processed.", { activityId: activity.id?.href, activity: json, recipient, }); return new Response( `Activity <${activity.id}> has already been processed.`, Loading @@ -544,6 +546,7 @@ export async function handleInbox<TContextData>( "The signer ({keyId}) and the actor ({actorId}) do not match.", { activity: json, recipient, keyId: httpSigKey.id?.href, actorId: activity.actorId.href, }, Loading @@ -561,14 +564,14 @@ export async function handleInbox<TContextData>( id: crypto.randomUUID(), baseUrl: request.url, activity: json, identifier, identifier: recipient, attempt: 0, started: new Date().toISOString(), } satisfies InboxMessage, ); logger.info( "Activity {activityId} is enqueued.", { activityId: activity.id?.href, activity: json }, { activityId: activity.id?.href, activity: json, recipient }, ); return new Response("Activity is enqueued.", { status: 202, Loading @@ -579,7 +582,7 @@ export async function handleInbox<TContextData>( if (listener == null) { logger.error( "Unsupported activity type:\n{activity}", { activity: json }, { activity: json, recipient }, ); return new Response("", { status: 202, Loading @@ -587,19 +590,29 @@ export async function handleInbox<TContextData>( }); } try { await listener(inboxContextFactory(json), activity); await listener(inboxContextFactory(recipient, json), activity); } catch (error) { try { await inboxErrorHandler?.(context, error as Error); } catch (error) { logger.error( "An unexpected error occurred in inbox error handler:\n{error}", { error, activityId: activity.id?.href, activity: json }, { error, activityId: activity.id?.href, activity: json, recipient, }, ); } logger.error( "Failed to process the incoming activity {activityId}:\n{error}", { error, activityId: activity.id?.href, activity: json }, { error, activityId: activity.id?.href, activity: json, recipient, }, ); return new Response("Internal server error.", { status: 500, Loading @@ -611,7 +624,7 @@ export async function handleInbox<TContextData>( } logger.info( "Activity {activityId} has been processed.", { activityId: activity.id?.href, activity: json }, { activityId: activity.id?.href, activity: json, recipient }, ); return new Response("", { status: 202, Loading src/federation/middleware.test.ts +4 −3 Original line number Diff line number Diff line Loading @@ -1217,7 +1217,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => { "id": "https://example.com/activity", "actor": "https://example.com/person2", }; const ctx = new InboxContextImpl(activity, { const ctx = new InboxContextImpl(null, activity, { data: undefined, federation, url: new URL("https://example.com/"), Loading @@ -1241,7 +1241,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => { "id": "https://example.com/activity", "actor": "https://example.com/person2", }; const ctx = new InboxContextImpl(activity, { const ctx = new InboxContextImpl(null, activity, { data: undefined, federation, url: new URL("https://example.com/"), Loading Loading @@ -1270,6 +1270,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => { { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader }, ); const ctx = new InboxContextImpl( null, await activity.toJsonLd({ contextLoader: mockDocumentLoader }), { data: undefined, Loading Loading @@ -1301,7 +1302,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => { rsaPublicKey3.id!, { contextLoader: mockDocumentLoader }, ); const ctx = new InboxContextImpl(activity, { const ctx = new InboxContextImpl(null, activity, { data: undefined, federation, url: new URL("https://example.com/"), Loading Loading
CHANGES.md +2 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,8 @@ Version 1.2.0 To be released. - Added `InboxContext.recipient` property. - Added NodeInfo client functions. - Added `getNodeInfo()` function. Loading
src/federation/context.ts +7 −0 Original line number Diff line number Diff line Loading @@ -384,6 +384,13 @@ export interface RequestContext<TContextData> extends Context<TContextData> { * @since 1.0.0 */ export interface InboxContext<TContextData> extends Context<TContextData> { /** * The identifier of the recipient of the inbox. If the inbox is a shared * inbox, it is `null`. * @since 1.2.0 */ recipient: string | null; /** * Forwards a received activity to the recipients' inboxes. The forwarded * activity will be signed in HTTP Signatures by the forwarder, but its Loading
src/federation/handler.test.ts +12 −12 Original line number Diff line number Diff line Loading @@ -1099,7 +1099,7 @@ test("handleInbox()", async () => { skipSignatureVerification: false, } as const; let response = await handleInbox(unsignedRequest, { identifier: null, recipient: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); Loading @@ -1112,10 +1112,10 @@ test("handleInbox()", async () => { onNotFoundCalled = null; response = await handleInbox(unsignedRequest, { identifier: "nobody", recipient: "nobody", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); return createInboxContext({ ...unsignedContext, recipient: "nobody" }); }, ...inboxOptions, }); Loading @@ -1124,7 +1124,7 @@ test("handleInbox()", async () => { onNotFoundCalled = null; response = await handleInbox(unsignedRequest, { identifier: null, recipient: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); Loading @@ -1135,10 +1135,10 @@ test("handleInbox()", async () => { assertEquals(response.status, 401); response = await handleInbox(unsignedRequest, { identifier: "someone", recipient: "someone", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); return createInboxContext({ ...unsignedContext, recipient: "someone" }); }, ...inboxOptions, }); Loading @@ -1158,7 +1158,7 @@ test("handleInbox()", async () => { documentLoader: mockDocumentLoader, }); response = await handleInbox(signedRequest, { identifier: null, recipient: null, context: signedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); Loading @@ -1169,10 +1169,10 @@ test("handleInbox()", async () => { assertEquals(response.status, 202); response = await handleInbox(signedRequest, { identifier: "someone", recipient: "someone", context: signedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); return createInboxContext({ ...unsignedContext, recipient: "someone" }); }, ...inboxOptions, }); Loading @@ -1180,7 +1180,7 @@ test("handleInbox()", async () => { assertEquals(response.status, 202); response = await handleInbox(unsignedRequest, { identifier: null, recipient: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); Loading @@ -1192,10 +1192,10 @@ test("handleInbox()", async () => { assertEquals(response.status, 202); response = await handleInbox(unsignedRequest, { identifier: "someone", recipient: "someone", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); return createInboxContext({ ...unsignedContext, recipient: "someone" }); }, ...inboxOptions, skipSignatureVerification: true, Loading
src/federation/handler.ts +37 −24 Original line number Diff line number Diff line Loading @@ -341,9 +341,10 @@ function filterCollectionItems<TItem extends Object | Link | Recipient | URL>( } export interface InboxHandlerParameters<TContextData> { identifier: string | null; recipient: string | null; context: RequestContext<TContextData>; inboxContextFactory( recipient: string | null, activity: unknown, ): InboxContext<TContextData>; kv: KvStore; Loading @@ -363,7 +364,7 @@ export interface InboxHandlerParameters<TContextData> { export async function handleInbox<TContextData>( request: Request, { identifier, recipient, context, inboxContextFactory, kv, Loading @@ -379,12 +380,12 @@ export async function handleInbox<TContextData>( ): Promise<Response> { const logger = getLogger(["fedify", "federation", "inbox"]); if (actorDispatcher == null) { logger.error("Actor dispatcher is not set.", { identifier }); logger.error("Actor dispatcher is not set.", { recipient }); return await onNotFound(request); } else if (identifier != null) { const actor = await actorDispatcher(context, identifier); } else if (recipient != null) { const actor = await actorDispatcher(context, recipient); if (actor == null) { logger.error("Actor {identifier} not found.", { identifier }); logger.error("Actor {recipient} not found.", { recipient }); return await onNotFound(request); } } Loading @@ -392,13 +393,13 @@ export async function handleInbox<TContextData>( try { json = await request.clone().json(); } catch (error) { logger.error("Failed to parse JSON:\n{error}", { identifier, error }); logger.error("Failed to parse JSON:\n{error}", { recipient, error }); try { await inboxErrorHandler?.(context, error as Error); } catch (error) { logger.error( "An unexpected error occurred in inbox error handler:\n{error}", { error, activity: json }, { error, activity: json, recipient }, ); } return new Response("Invalid JSON.", { Loading Loading @@ -437,12 +438,12 @@ export async function handleInbox<TContextData>( const jsonWithoutSig = detachSignature(json); let activity: Activity | null = null; if (ldSigVerified) { logger.debug("Linked Data Signatures are verified.", { identifier, json }); logger.debug("Linked Data Signatures are verified.", { recipient, json }); activity = await Activity.fromJsonLd(jsonWithoutSig, context); } else { logger.debug( "Linked Data Signatures are not verified.", { identifier, json }, { recipient, json }, ); try { activity = await verifyObject(Activity, jsonWithoutSig, { Loading @@ -452,8 +453,8 @@ export async function handleInbox<TContextData>( }); } catch (error) { logger.error("Failed to parse activity:\n{error}", { identifier, json, recipient, activity: json, error, }); try { Loading @@ -461,7 +462,7 @@ export async function handleInbox<TContextData>( } catch (error) { logger.error( "An unexpected error occurred in inbox error handler:\n{error}", { error, activity: json }, { error, activity: json, recipient }, ); } return new Response("Invalid activity.", { Loading @@ -472,12 +473,12 @@ export async function handleInbox<TContextData>( if (activity == null) { logger.debug( "Object Integrity Proofs are not verified.", { identifier, json }, { recipient, activity: json }, ); } else { logger.debug( "Object Integrity Proofs are verified.", { identifier, json }, { recipient, activity: json }, ); } } Loading @@ -493,7 +494,7 @@ export async function handleInbox<TContextData>( if (key == null) { logger.error( "Failed to verify the request's HTTP Signatures.", { identifier }, { recipient }, ); const response = new Response( "Failed to verify the request signature.", Loading @@ -504,7 +505,7 @@ export async function handleInbox<TContextData>( ); return response; } else { logger.debug("HTTP Signatures are verified.", { identifier }); logger.debug("HTTP Signatures are verified.", { recipient }); } httpSigKey = key; } Loading @@ -519,6 +520,7 @@ export async function handleInbox<TContextData>( logger.debug("Activity {activityId} has already been processed.", { activityId: activity.id?.href, activity: json, recipient, }); return new Response( `Activity <${activity.id}> has already been processed.`, Loading @@ -544,6 +546,7 @@ export async function handleInbox<TContextData>( "The signer ({keyId}) and the actor ({actorId}) do not match.", { activity: json, recipient, keyId: httpSigKey.id?.href, actorId: activity.actorId.href, }, Loading @@ -561,14 +564,14 @@ export async function handleInbox<TContextData>( id: crypto.randomUUID(), baseUrl: request.url, activity: json, identifier, identifier: recipient, attempt: 0, started: new Date().toISOString(), } satisfies InboxMessage, ); logger.info( "Activity {activityId} is enqueued.", { activityId: activity.id?.href, activity: json }, { activityId: activity.id?.href, activity: json, recipient }, ); return new Response("Activity is enqueued.", { status: 202, Loading @@ -579,7 +582,7 @@ export async function handleInbox<TContextData>( if (listener == null) { logger.error( "Unsupported activity type:\n{activity}", { activity: json }, { activity: json, recipient }, ); return new Response("", { status: 202, Loading @@ -587,19 +590,29 @@ export async function handleInbox<TContextData>( }); } try { await listener(inboxContextFactory(json), activity); await listener(inboxContextFactory(recipient, json), activity); } catch (error) { try { await inboxErrorHandler?.(context, error as Error); } catch (error) { logger.error( "An unexpected error occurred in inbox error handler:\n{error}", { error, activityId: activity.id?.href, activity: json }, { error, activityId: activity.id?.href, activity: json, recipient, }, ); } logger.error( "Failed to process the incoming activity {activityId}:\n{error}", { error, activityId: activity.id?.href, activity: json }, { error, activityId: activity.id?.href, activity: json, recipient, }, ); return new Response("Internal server error.", { status: 500, Loading @@ -611,7 +624,7 @@ export async function handleInbox<TContextData>( } logger.info( "Activity {activityId} has been processed.", { activityId: activity.id?.href, activity: json }, { activityId: activity.id?.href, activity: json, recipient }, ); return new Response("", { status: 202, Loading
src/federation/middleware.test.ts +4 −3 Original line number Diff line number Diff line Loading @@ -1217,7 +1217,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => { "id": "https://example.com/activity", "actor": "https://example.com/person2", }; const ctx = new InboxContextImpl(activity, { const ctx = new InboxContextImpl(null, activity, { data: undefined, federation, url: new URL("https://example.com/"), Loading @@ -1241,7 +1241,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => { "id": "https://example.com/activity", "actor": "https://example.com/person2", }; const ctx = new InboxContextImpl(activity, { const ctx = new InboxContextImpl(null, activity, { data: undefined, federation, url: new URL("https://example.com/"), Loading Loading @@ -1270,6 +1270,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => { { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader }, ); const ctx = new InboxContextImpl( null, await activity.toJsonLd({ contextLoader: mockDocumentLoader }), { data: undefined, Loading Loading @@ -1301,7 +1302,7 @@ test("InboxContextImpl.forwardActivity()", async (t) => { rsaPublicKey3.id!, { contextLoader: mockDocumentLoader }, ); const ctx = new InboxContextImpl(activity, { const ctx = new InboxContextImpl(null, activity, { data: undefined, federation, url: new URL("https://example.com/"), Loading