From 554421e9337a2f7e0fe3626f67dc5e283301185f Mon Sep 17 00:00:00 2001 From: "Kim, Hyeonseo" Date: Mon, 1 Sep 2025 22:48:29 +0900 Subject: [PATCH 1/4] type: change ResourceDescriptor and Link interfaces to support OStatusSubscribeLink and make href optional --- packages/fedify/src/webfinger/jrd.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/fedify/src/webfinger/jrd.ts b/packages/fedify/src/webfinger/jrd.ts index 7111f755d..38a116ffe 100644 --- a/packages/fedify/src/webfinger/jrd.ts +++ b/packages/fedify/src/webfinger/jrd.ts @@ -21,7 +21,7 @@ export interface ResourceDescriptor { /** * Links to other resources. */ - links?: Link[]; + links?: Array; } /** @@ -44,7 +44,7 @@ export interface Link { /** * A URI pointing to the target resource. */ - href: string; + href?: string; /** * Human-readable titles describing the link relation. If the language is @@ -57,3 +57,17 @@ export interface Link { */ properties?: Record; } + +/** + * References a link. See also + * [OStatus 1.0 Draft 2](https://www.w3.org/community/ostatus/wiki/images/9/93/OStatus_1.0_Draft_2.pdf) + */ +export interface OStatusSubscribeLink { + rel: "http://ostatus.org/schema/1.0/subscribe"; + /** + * A URI template (RFC 6570) that can be used to construct URIs by + * substituting variables. Used primarily for subscription endpoints + * where parameters like account URIs need to be dynamically inserted. + */ + template: string; +} From d2af1f0528eadef58eda014ea83908ec1adb503a Mon Sep 17 00:00:00 2001 From: "Kim, Hyeonseo" Date: Tue, 2 Sep 2025 17:42:13 +0900 Subject: [PATCH 2/4] feat: add isLink and isOStatusSubscribeLink functions with condition support --- packages/fedify/src/mod.ts | 7 +++++- packages/fedify/src/webfinger/jrd.ts | 35 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/fedify/src/mod.ts b/packages/fedify/src/mod.ts index 8a0d19f8c..bf603ddb3 100644 --- a/packages/fedify/src/mod.ts +++ b/packages/fedify/src/mod.ts @@ -48,4 +48,9 @@ export * from "./nodeinfo/mod.ts"; export * from "./runtime/mod.ts"; export * from "./sig/mod.ts"; export * from "./vocab/mod.ts"; -export { lookupWebFinger, type ResourceDescriptor } from "./webfinger/mod.ts"; +export { + isLink, + isOStatusSubscribeLink, + lookupWebFinger, + type ResourceDescriptor, +} from "./webfinger/mod.ts"; diff --git a/packages/fedify/src/webfinger/jrd.ts b/packages/fedify/src/webfinger/jrd.ts index 38a116ffe..0d8898aec 100644 --- a/packages/fedify/src/webfinger/jrd.ts +++ b/packages/fedify/src/webfinger/jrd.ts @@ -71,3 +71,38 @@ export interface OStatusSubscribeLink { */ template: string; } + +export function isOStatusSubscribeLink( + link: Link | OStatusSubscribeLink, +): link is OStatusSubscribeLink { + if ( + "template" in link && + link.rel === "http://ostatus.org/schema/1.0/subscribe" && + typeof link.template === "string" + ) return true; + return false; +} + +isOStatusSubscribeLink.withCondition = ( + condition: (link: OStatusSubscribeLink) => boolean, +) => +(link: Link | OStatusSubscribeLink): link is OStatusSubscribeLink => { + return isOStatusSubscribeLink(link) && condition(link); +}; + +export function isLink( + link: Link | OStatusSubscribeLink, +): link is Link { + if ( + "rel" in link && + typeof link.rel === "string" + ) return true; + return false; +} + +isLink.withCondition = ( + condition: (link: Link) => boolean, +) => +(link: Link | Link): link is Link => { + return isLink(link) && condition(link); +}; From be98dc79c8918a7cf660876b66148a201f2a0c6a Mon Sep 17 00:00:00 2001 From: "Kim, Hyeonseo" Date: Tue, 2 Sep 2025 18:04:40 +0900 Subject: [PATCH 3/4] feat: enhance link handling with isLink and isOStatusSubscribeLink functions --- docs/manual/context.md | 9 ++++++--- packages/fedify/src/nodeinfo/client.ts | 20 +++++++++++--------- packages/fedify/src/vocab/lookup.ts | 5 ++++- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/docs/manual/context.md b/docs/manual/context.md index f3266300f..ee203a8cc 100644 --- a/docs/manual/context.md +++ b/docs/manual/context.md @@ -435,14 +435,17 @@ The returned WebFinger document contains links to various resources associated with the account, such as profile pages, ActivityPub actor URIs, and more: ~~~~ typescript twoslash -import { type Context } from "@fedify/fedify"; +import { type Context, isLink } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- const webfingerData = await ctx.lookupWebFinger("acct:fedify@hollo.social"); // Find the ActivityPub actor URI -const activityPubActorLink = webfingerData?.links?.find(link => - link.rel === "self" && link.type === "application/activity+json" +const activityPubActorLink = webfingerData?.links?.find( + isLink.withCondition((link) => + link.rel === "self" && + link.type === "application/activity+json" + ), ); if (activityPubActorLink?.href) { diff --git a/packages/fedify/src/nodeinfo/client.ts b/packages/fedify/src/nodeinfo/client.ts index 4bb663a97..02f1849ee 100644 --- a/packages/fedify/src/nodeinfo/client.ts +++ b/packages/fedify/src/nodeinfo/client.ts @@ -3,7 +3,7 @@ import { getUserAgent, type GetUserAgentOptions, } from "../runtime/docloader.ts"; -import type { ResourceDescriptor } from "../webfinger/jrd.ts"; +import { isLink, type ResourceDescriptor } from "../webfinger/jrd.ts"; import { parseSemVer, type SemVer } from "./semver.ts"; import type { InboundService, @@ -113,15 +113,17 @@ export async function getNodeInfo( return undefined; } const wellKnownRd = await wellKnownResponse.json() as ResourceDescriptor; - const link = wellKnownRd?.links?.find((link) => - link != null && - "rel" in link && - (link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0" || - link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1") && - "href" in link && - link.href != null + const link = wellKnownRd?.links?.find( + isLink.withCondition((link) => + link != null && + "rel" in link && + (link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0" || + link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1") && + "href" in link && + link.href != null + ), ); - if (link == null) { + if (link == null || link.href == null) { logger.error( "Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", { url: wellKnownUrl.href, resourceDescriptor: wellKnownRd }, diff --git a/packages/fedify/src/vocab/lookup.ts b/packages/fedify/src/vocab/lookup.ts index 51dbd0e3b..2c02a19ed 100644 --- a/packages/fedify/src/vocab/lookup.ts +++ b/packages/fedify/src/vocab/lookup.ts @@ -7,6 +7,7 @@ import { getDocumentLoader, type GetUserAgentOptions, } from "../runtime/docloader.ts"; +import { isOStatusSubscribeLink } from "../webfinger/jrd.ts"; import { lookupWebFinger } from "../webfinger/lookup.ts"; import { toAcctUrl } from "./handle.ts"; import { getTypeId } from "./type.ts"; @@ -158,10 +159,12 @@ async function lookupObjectInternal( if (jrd?.links == null) return null; for (const l of jrd.links) { if ( + isOStatusSubscribeLink(l) || l.type !== "application/activity+json" && !l.type?.match( /application\/ld\+json;\s*profile="https:\/\/www.w3.org\/ns\/activitystreams"/, - ) || l.rel !== "self" + ) || + l.rel !== "self" || l.href == null ) continue; try { const remoteDoc = await documentLoader(l.href, { From 713ff700bf7bf857f1796e329aff3aef8e2c6154 Mon Sep 17 00:00:00 2001 From: "Kim, Hyeonseo" Date: Tue, 2 Sep 2025 18:11:31 +0900 Subject: [PATCH 4/4] docs: add example about OStatusSubscribeLink --- docs/manual/context.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/manual/context.md b/docs/manual/context.md index ee203a8cc..abc1e192c 100644 --- a/docs/manual/context.md +++ b/docs/manual/context.md @@ -435,7 +435,7 @@ The returned WebFinger document contains links to various resources associated with the account, such as profile pages, ActivityPub actor URIs, and more: ~~~~ typescript twoslash -import { type Context, isLink } from "@fedify/fedify"; +import { type Context, isLink, isOStatusSubscribeLink } from "@fedify/fedify"; const ctx = null as unknown as Context; // ---cut-before--- const webfingerData = await ctx.lookupWebFinger("acct:fedify@hollo.social"); @@ -452,6 +452,16 @@ if (activityPubActorLink?.href) { const actor = await ctx.lookupObject(activityPubActorLink.href); // Work with the actor... } + +// Get subscription template URI +const activityPubSubscribeLink = webfingerData?.links?.find((link) => + isOStatusSubscribeLink(link) +); + +if (activityPubSubscribeLink?.template) { + // Work with remote follow URI... +} + ~~~~ > [!NOTE]