Loading docs/manual/nodeinfo.md +6 −6 Original line number Diff line number Diff line Loading @@ -163,9 +163,9 @@ You may want to fetch NodeInfo objects from other servers. Fedify provides a way to fetch NodeInfo objects with the `getNodeInfo()` function: ~~~~ typescript twoslash import { type NodeInfo, getNodeInfo } from "@fedify/fedify"; import { getNodeInfo } from "@fedify/fedify"; const nodeInfo: NodeInfo | null = await getNodeInfo("https://example.com/"); const nodeInfo = await getNodeInfo("https://example.com/"); if (nodeInfo != null) console.log(nodeInfo); ~~~~ Loading @@ -174,14 +174,14 @@ a NodeInfo endpoint and the response is valid. Otherwise, it returns `null`. > [!TIP] > Sometimes, a server may provide a slightly invalid NodeInfo object. In such > case, you can enforce parsing the object by passing `{ tryBestEffort: true }` > case, you can enforce parsing the object by passing `{ parse: "best-effort" }` > option as the second argument to `getNodeInfo()` function: > > ~~~~ typescript twoslash > import { type NodeInfo, getNodeInfo } from "@fedify/fedify"; > import { getNodeInfo } from "@fedify/fedify"; > > const nodeInfo: NodeInfo | null = await getNodeInfo("https://example.com/", { > tryBestEffort: true, > const nodeInfo = await getNodeInfo("https://example.com/", { > parse: "best-effort", > }); > > if (nodeInfo != null) console.log(nodeInfo); Loading src/nodeinfo/client.test.ts +14 −9 Original line number Diff line number Diff line Loading @@ -35,14 +35,16 @@ test("getNodeInfo()", async (t) => { ); }); mf.mock("GET@/nodeinfo/2.1", (req) => { assertEquals(new URL(req.url).host, "example.com"); return new Response( JSON.stringify({ const rawExpected = { software: { name: "foo", version: "1.2.3" }, protocols: ["activitypub", "diaspora"], usage: { users: {}, localPosts: 123, localComments: 456 }, }), }; mf.mock("GET@/nodeinfo/2.1", (req) => { assertEquals(new URL(req.url).host, "example.com"); return new Response( JSON.stringify(rawExpected), ); }); Loading @@ -63,6 +65,9 @@ test("getNodeInfo()", async (t) => { await t.step("indirect", async () => { const info = await getNodeInfo("https://example.com/"); assertEquals(info, expected); const raw = await getNodeInfo("https://example.com/", { parse: "none" }); assertEquals(raw, rawExpected); }); await t.step("direct", async () => { Loading @@ -82,7 +87,7 @@ test("getNodeInfo()", async (t) => { await t.step("indirect: no links", async () => { const info = await getNodeInfo("https://example.com/"); assertEquals(info, null); assertEquals(info, undefined); }); mf.mock("GET@/.well-known/nodeinfo", (req) => { Loading @@ -92,7 +97,7 @@ test("getNodeInfo()", async (t) => { await t.step("indirect: 404", async () => { const info = await getNodeInfo("https://example.com/"); assertEquals(info, null); assertEquals(info, undefined); }); await t.step("direct: 404", async () => { Loading @@ -100,12 +105,12 @@ test("getNodeInfo()", async (t) => { "https://example.com/nodeinfo/2.0", { direct: true }, ); assertEquals(info, null); assertEquals(info, undefined); const info2 = await getNodeInfo( "https://example.com/404", { direct: true }, ); assertEquals(info2, null); assertEquals(info2, undefined); }); mf.uninstall(); Loading src/nodeinfo/client.ts +45 −8 Original line number Diff line number Diff line Loading @@ -18,7 +18,7 @@ const logger = getLogger(["fedify", "nodeinfo", "client"]); * Options for {@link getNodeInfo} function. * @since 1.2.0 */ export interface GetNodeInfoOptions extends ParseNodeInfoOptions { export interface GetNodeInfoOptions { /** * Whether to directly fetch the NodeInfo document from the given URL. * Otherwise, the NodeInfo document will be fetched from the `.well-known` Loading @@ -27,6 +27,18 @@ export interface GetNodeInfoOptions extends ParseNodeInfoOptions { * Turned off by default. */ direct?: boolean; /** * How strictly to parse the NodeInfo document. * * - `"strict"`: Parse the NodeInfo document strictly. If the document is * invalid, `undefined` is returned. This is the default. * - `"best-effort"`: Try to parse the NodeInfo document even if it is * invalid. * - `"none"`: Do not parse the NodeInfo document. The function will return * the raw JSON value. */ parse?: "strict" | "best-effort" | "none"; } /** Loading @@ -38,13 +50,35 @@ export interface GetNodeInfoOptions extends ParseNodeInfoOptions { * the NodeInfo document will be fetched from the given URL. * @param options Options for fetching the NodeInfo document. * @returns The NodeInfo document if it could be fetched successfully. * Otherwise, `null` is returned. * Otherwise, `undefined` is returned. * @since 1.2.0 */ export async function getNodeInfo( url: URL | string, options?: GetNodeInfoOptions & { parse?: "strict" | "best-effort" }, ): Promise<NodeInfo | undefined>; /** * Fetches a NodeInfo document from the given URL. * @param url The base URL of the server. If `options.direct` is turned off * (default), the NodeInfo document will be fetched from * the `.well-known` location of this URL (hence the only origin * of the URL is used). If `options.direct` is turned on, * the NodeInfo document will be fetched from the given URL. * @param options Options for fetching the NodeInfo document. * @returns The NodeInfo document if it could be fetched successfully. * Otherwise, `undefined` is returned. * @since 1.2.0 */ export async function getNodeInfo( url: URL | string, options: GetNodeInfoOptions & { parse: "none" }, ): Promise<JsonValue | undefined>; export async function getNodeInfo( url: URL | string, options: GetNodeInfoOptions = {}, ): Promise<NodeInfo | null> { ): Promise<NodeInfo | JsonValue | undefined> { try { let nodeInfoUrl: URL | string = url; if (!options.direct) { Loading @@ -56,7 +90,7 @@ export async function getNodeInfo( status: wellKnownResponse.status, statusText: wellKnownResponse.statusText, }); return null; return undefined; } const wellKnownRd = await wellKnownResponse.json() as ResourceDescriptor; const link = wellKnownRd?.links?.find((link) => Loading @@ -72,7 +106,7 @@ export async function getNodeInfo( "Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", { url: wellKnownUrl.href, resourceDescriptor: wellKnownRd }, ); return null; return undefined; } nodeInfoUrl = link.href; } Loading @@ -86,16 +120,19 @@ export async function getNodeInfo( statusText: response.statusText, }, ); return null; return undefined; } const data = await response.json(); return parseNodeInfo(data, options); if (options.parse === "none") return data as JsonValue; return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort", }) ?? undefined; } catch (error) { logger.error("Failed to fetch NodeInfo document from {url}: {error}", { url: url.toString(), error, }); return null; return undefined; } } Loading Loading
docs/manual/nodeinfo.md +6 −6 Original line number Diff line number Diff line Loading @@ -163,9 +163,9 @@ You may want to fetch NodeInfo objects from other servers. Fedify provides a way to fetch NodeInfo objects with the `getNodeInfo()` function: ~~~~ typescript twoslash import { type NodeInfo, getNodeInfo } from "@fedify/fedify"; import { getNodeInfo } from "@fedify/fedify"; const nodeInfo: NodeInfo | null = await getNodeInfo("https://example.com/"); const nodeInfo = await getNodeInfo("https://example.com/"); if (nodeInfo != null) console.log(nodeInfo); ~~~~ Loading @@ -174,14 +174,14 @@ a NodeInfo endpoint and the response is valid. Otherwise, it returns `null`. > [!TIP] > Sometimes, a server may provide a slightly invalid NodeInfo object. In such > case, you can enforce parsing the object by passing `{ tryBestEffort: true }` > case, you can enforce parsing the object by passing `{ parse: "best-effort" }` > option as the second argument to `getNodeInfo()` function: > > ~~~~ typescript twoslash > import { type NodeInfo, getNodeInfo } from "@fedify/fedify"; > import { getNodeInfo } from "@fedify/fedify"; > > const nodeInfo: NodeInfo | null = await getNodeInfo("https://example.com/", { > tryBestEffort: true, > const nodeInfo = await getNodeInfo("https://example.com/", { > parse: "best-effort", > }); > > if (nodeInfo != null) console.log(nodeInfo); Loading
src/nodeinfo/client.test.ts +14 −9 Original line number Diff line number Diff line Loading @@ -35,14 +35,16 @@ test("getNodeInfo()", async (t) => { ); }); mf.mock("GET@/nodeinfo/2.1", (req) => { assertEquals(new URL(req.url).host, "example.com"); return new Response( JSON.stringify({ const rawExpected = { software: { name: "foo", version: "1.2.3" }, protocols: ["activitypub", "diaspora"], usage: { users: {}, localPosts: 123, localComments: 456 }, }), }; mf.mock("GET@/nodeinfo/2.1", (req) => { assertEquals(new URL(req.url).host, "example.com"); return new Response( JSON.stringify(rawExpected), ); }); Loading @@ -63,6 +65,9 @@ test("getNodeInfo()", async (t) => { await t.step("indirect", async () => { const info = await getNodeInfo("https://example.com/"); assertEquals(info, expected); const raw = await getNodeInfo("https://example.com/", { parse: "none" }); assertEquals(raw, rawExpected); }); await t.step("direct", async () => { Loading @@ -82,7 +87,7 @@ test("getNodeInfo()", async (t) => { await t.step("indirect: no links", async () => { const info = await getNodeInfo("https://example.com/"); assertEquals(info, null); assertEquals(info, undefined); }); mf.mock("GET@/.well-known/nodeinfo", (req) => { Loading @@ -92,7 +97,7 @@ test("getNodeInfo()", async (t) => { await t.step("indirect: 404", async () => { const info = await getNodeInfo("https://example.com/"); assertEquals(info, null); assertEquals(info, undefined); }); await t.step("direct: 404", async () => { Loading @@ -100,12 +105,12 @@ test("getNodeInfo()", async (t) => { "https://example.com/nodeinfo/2.0", { direct: true }, ); assertEquals(info, null); assertEquals(info, undefined); const info2 = await getNodeInfo( "https://example.com/404", { direct: true }, ); assertEquals(info2, null); assertEquals(info2, undefined); }); mf.uninstall(); Loading
src/nodeinfo/client.ts +45 −8 Original line number Diff line number Diff line Loading @@ -18,7 +18,7 @@ const logger = getLogger(["fedify", "nodeinfo", "client"]); * Options for {@link getNodeInfo} function. * @since 1.2.0 */ export interface GetNodeInfoOptions extends ParseNodeInfoOptions { export interface GetNodeInfoOptions { /** * Whether to directly fetch the NodeInfo document from the given URL. * Otherwise, the NodeInfo document will be fetched from the `.well-known` Loading @@ -27,6 +27,18 @@ export interface GetNodeInfoOptions extends ParseNodeInfoOptions { * Turned off by default. */ direct?: boolean; /** * How strictly to parse the NodeInfo document. * * - `"strict"`: Parse the NodeInfo document strictly. If the document is * invalid, `undefined` is returned. This is the default. * - `"best-effort"`: Try to parse the NodeInfo document even if it is * invalid. * - `"none"`: Do not parse the NodeInfo document. The function will return * the raw JSON value. */ parse?: "strict" | "best-effort" | "none"; } /** Loading @@ -38,13 +50,35 @@ export interface GetNodeInfoOptions extends ParseNodeInfoOptions { * the NodeInfo document will be fetched from the given URL. * @param options Options for fetching the NodeInfo document. * @returns The NodeInfo document if it could be fetched successfully. * Otherwise, `null` is returned. * Otherwise, `undefined` is returned. * @since 1.2.0 */ export async function getNodeInfo( url: URL | string, options?: GetNodeInfoOptions & { parse?: "strict" | "best-effort" }, ): Promise<NodeInfo | undefined>; /** * Fetches a NodeInfo document from the given URL. * @param url The base URL of the server. If `options.direct` is turned off * (default), the NodeInfo document will be fetched from * the `.well-known` location of this URL (hence the only origin * of the URL is used). If `options.direct` is turned on, * the NodeInfo document will be fetched from the given URL. * @param options Options for fetching the NodeInfo document. * @returns The NodeInfo document if it could be fetched successfully. * Otherwise, `undefined` is returned. * @since 1.2.0 */ export async function getNodeInfo( url: URL | string, options: GetNodeInfoOptions & { parse: "none" }, ): Promise<JsonValue | undefined>; export async function getNodeInfo( url: URL | string, options: GetNodeInfoOptions = {}, ): Promise<NodeInfo | null> { ): Promise<NodeInfo | JsonValue | undefined> { try { let nodeInfoUrl: URL | string = url; if (!options.direct) { Loading @@ -56,7 +90,7 @@ export async function getNodeInfo( status: wellKnownResponse.status, statusText: wellKnownResponse.statusText, }); return null; return undefined; } const wellKnownRd = await wellKnownResponse.json() as ResourceDescriptor; const link = wellKnownRd?.links?.find((link) => Loading @@ -72,7 +106,7 @@ export async function getNodeInfo( "Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", { url: wellKnownUrl.href, resourceDescriptor: wellKnownRd }, ); return null; return undefined; } nodeInfoUrl = link.href; } Loading @@ -86,16 +120,19 @@ export async function getNodeInfo( statusText: response.statusText, }, ); return null; return undefined; } const data = await response.json(); return parseNodeInfo(data, options); if (options.parse === "none") return data as JsonValue; return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort", }) ?? undefined; } catch (error) { logger.error("Failed to fetch NodeInfo document from {url}: {error}", { url: url.toString(), error, }); return null; return undefined; } } Loading