Loading .vscode/settings.json +1 −0 Original line number Diff line number Diff line Loading @@ -68,6 +68,7 @@ "spki", "subproperty", "superproperty", "tempserver", "unfollow", "unfollowing", "urlpattern", Loading cli/lookup.ts +84 −8 Original line number Diff line number Diff line import { Command } from "@cliffy/command"; import { lookupObject } from "@fedify/fedify"; import { Application, CryptographicKey, type DocumentLoader, generateCryptoKeyPair, getAuthenticatedDocumentLoader, lookupObject, type ResourceDescriptor, respondWithObject, } from "@fedify/fedify"; import { highlight } from "cli-highlight"; import ora from "ora"; import { spawnTemporaryServer, type TemporaryServer } from "./tempserver.ts"; export const command = new Command() .arguments("<url:string>") Loading @@ -9,6 +20,7 @@ export const command = new Command() "The argument can be either a URL or an actor handle " + "(e.g., @username@domain).", ) .option("-a, --authorized-fetch", "Sign the request with an one-time key.") .option("-c, --compact", "Compact the fetched JSON-LD document.", { conflicts: ["expand"], }) Loading @@ -16,14 +28,78 @@ export const command = new Command() conflicts: ["compact"], }) .action(async (options, url: string) => { const object = await lookupObject(url); const spinner = ora({ text: "Looking up the object...", discardStdin: false, }).start(); let server: TemporaryServer | undefined = undefined; let loader: DocumentLoader | undefined = undefined; if (options.authorizedFetch) { spinner.text = "Generating a one-time key pair..."; const key = await generateCryptoKeyPair(); spinner.text = "Spinning up a temporary ActivityPub server..."; server = await spawnTemporaryServer((req) => { if (new URL(req.url).pathname == "/.well-known/webfinger") { const jrd: ResourceDescriptor = { subject: `acct:${server!.url.hostname}@${server!.url.hostname}`, aliases: [server!.url.href], links: [ { rel: "self", href: server!.url.href, type: "application/activity+json", }, ], }; return new Response(JSON.stringify(jrd), { headers: { "Content-Type": "application/jrd+json" }, }); } return respondWithObject( new Application({ id: server?.url, preferredUsername: server?.url?.hostname, publicKey: new CryptographicKey({ id: new URL("#main-key", server?.url), owner: server?.url, publicKey: key.publicKey, }), manuallyApprovesFollowers: true, inbox: new URL("/inbox", server?.url), outbox: new URL("/outbox", server?.url), }), ); }); loader = getAuthenticatedDocumentLoader({ keyId: new URL("#main-key", server.url), privateKey: key.privateKey, }); } try { spinner.text = "Looking up the object..."; const object = await lookupObject(url, { documentLoader: loader }); spinner.succeed(); if (object == null) { console.error("Failed to fetch the object."); if (loader == null) { console.error( "It may be a private object. Try with -a/--authorized-fetch.", ); } Deno.exit(1); } if (options.compact) { printJson(await object?.toJsonLd()); printJson(await object.toJsonLd()); } else if (options.expand) { printJson(await object?.toJsonLd({ expand: true })); printJson(await object.toJsonLd({ expand: true })); } else { console.log(object); } } catch (_) { spinner.fail(); } finally { await server?.close(); } }); function printJson(json: unknown): void { Loading cli/tempserver.ts 0 → 100644 +38 −0 Original line number Diff line number Diff line import { openTunnel } from "@hongminhee/localtunnel"; import { getLogger } from "@logtape/logtape"; const logger = getLogger(["fedify", "cli", "tempserver"]); export interface TemporaryServer { url: URL; close(): Promise<void>; } export function spawnTemporaryServer( handler: Deno.ServeHandler, ): Promise<TemporaryServer> { return new Promise((resolve) => { const ac = new AbortController(); const server = Deno.serve({ handler, port: 0, signal: ac.signal, onListen({ port }) { logger.debug("Temporary server is listening on port {port}.", { port }); openTunnel({ port }).then((tun) => { logger.debug( "Temporary server is tunneled to {url}.", { url: tun.url.href }, ); resolve({ url: tun.url, async close() { await server.shutdown(); await tun.close(); }, }); }); }, }); }); } deno.json +4 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ "imports": { "@cfworker/json-schema": "npm:@cfworker/json-schema@^1.12.8", "@cliffy/command": "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts", "@cliffy/prompt": "https://deno.land/x/cliffy@v1.0.0-rc.4/prompt/mod.ts", "@david/which-runtime": "jsr:@david/which-runtime@^0.2.0", "@deno/dnt": "jsr:@deno/dnt@^0.41.1", "@fedify/fedify": "./mod.ts", Loading @@ -29,6 +30,7 @@ "@fedify/fedify/x/fresh": "./x/fresh.ts", "@fedify/fedify/x/hono": "./x/hono.ts", "@hongminhee/aitertools": "jsr:@hongminhee/aitertools@^0.6.0", "@hongminhee/localtunnel": "jsr:@hongminhee/localtunnel@^0.1.0", "@logtape/logtape": "jsr:@logtape/logtape@^0.2.2", "@phensley/language-tag": "npm:@phensley/language-tag@^1.8.0", "@std/assert": "jsr:@std/assert@^0.220.1", Loading @@ -51,6 +53,7 @@ "fast-check": "npm:fast-check@^3.17.0", "jsonld": "npm:jsonld@^8.3.2", "mock_fetch": "https://deno.land/x/mock_fetch@0.3.0/mod.ts", "ora": "npm:ora@^8.0.1", "uri-template-router": "npm:uri-template-router@^0.0.16", "url-template": "npm:url-template@^3.1.1" }, Loading @@ -68,7 +71,7 @@ "cache": "deno task codegen && deno cache mod.ts", "check": "deno task codegen && deno fmt --check && deno lint && deno check */*.ts", "codegen": "deno run --allow-read --allow-write --check codegen/main.ts vocab/ ../runtime/ > vocab/vocab.ts && deno fmt vocab/vocab.ts && deno cache vocab/vocab.ts && deno check vocab/vocab.ts", "cli": "deno task codegen && deno run --allow-read --allow-net --allow-env cli/mod.ts", "cli": "deno task codegen && deno run --allow-read --allow-net --allow-env --allow-run cli/mod.ts", "test-without-codegen": "deno test --check --doc --allow-read --allow-write --unstable-kv --trace-leaks", "test": "deno task codegen && deno task test-without-codegen", "coverage": "rm -rf coverage/ && deno task test --coverage && deno coverage --html coverage", Loading Loading
.vscode/settings.json +1 −0 Original line number Diff line number Diff line Loading @@ -68,6 +68,7 @@ "spki", "subproperty", "superproperty", "tempserver", "unfollow", "unfollowing", "urlpattern", Loading
cli/lookup.ts +84 −8 Original line number Diff line number Diff line import { Command } from "@cliffy/command"; import { lookupObject } from "@fedify/fedify"; import { Application, CryptographicKey, type DocumentLoader, generateCryptoKeyPair, getAuthenticatedDocumentLoader, lookupObject, type ResourceDescriptor, respondWithObject, } from "@fedify/fedify"; import { highlight } from "cli-highlight"; import ora from "ora"; import { spawnTemporaryServer, type TemporaryServer } from "./tempserver.ts"; export const command = new Command() .arguments("<url:string>") Loading @@ -9,6 +20,7 @@ export const command = new Command() "The argument can be either a URL or an actor handle " + "(e.g., @username@domain).", ) .option("-a, --authorized-fetch", "Sign the request with an one-time key.") .option("-c, --compact", "Compact the fetched JSON-LD document.", { conflicts: ["expand"], }) Loading @@ -16,14 +28,78 @@ export const command = new Command() conflicts: ["compact"], }) .action(async (options, url: string) => { const object = await lookupObject(url); const spinner = ora({ text: "Looking up the object...", discardStdin: false, }).start(); let server: TemporaryServer | undefined = undefined; let loader: DocumentLoader | undefined = undefined; if (options.authorizedFetch) { spinner.text = "Generating a one-time key pair..."; const key = await generateCryptoKeyPair(); spinner.text = "Spinning up a temporary ActivityPub server..."; server = await spawnTemporaryServer((req) => { if (new URL(req.url).pathname == "/.well-known/webfinger") { const jrd: ResourceDescriptor = { subject: `acct:${server!.url.hostname}@${server!.url.hostname}`, aliases: [server!.url.href], links: [ { rel: "self", href: server!.url.href, type: "application/activity+json", }, ], }; return new Response(JSON.stringify(jrd), { headers: { "Content-Type": "application/jrd+json" }, }); } return respondWithObject( new Application({ id: server?.url, preferredUsername: server?.url?.hostname, publicKey: new CryptographicKey({ id: new URL("#main-key", server?.url), owner: server?.url, publicKey: key.publicKey, }), manuallyApprovesFollowers: true, inbox: new URL("/inbox", server?.url), outbox: new URL("/outbox", server?.url), }), ); }); loader = getAuthenticatedDocumentLoader({ keyId: new URL("#main-key", server.url), privateKey: key.privateKey, }); } try { spinner.text = "Looking up the object..."; const object = await lookupObject(url, { documentLoader: loader }); spinner.succeed(); if (object == null) { console.error("Failed to fetch the object."); if (loader == null) { console.error( "It may be a private object. Try with -a/--authorized-fetch.", ); } Deno.exit(1); } if (options.compact) { printJson(await object?.toJsonLd()); printJson(await object.toJsonLd()); } else if (options.expand) { printJson(await object?.toJsonLd({ expand: true })); printJson(await object.toJsonLd({ expand: true })); } else { console.log(object); } } catch (_) { spinner.fail(); } finally { await server?.close(); } }); function printJson(json: unknown): void { Loading
cli/tempserver.ts 0 → 100644 +38 −0 Original line number Diff line number Diff line import { openTunnel } from "@hongminhee/localtunnel"; import { getLogger } from "@logtape/logtape"; const logger = getLogger(["fedify", "cli", "tempserver"]); export interface TemporaryServer { url: URL; close(): Promise<void>; } export function spawnTemporaryServer( handler: Deno.ServeHandler, ): Promise<TemporaryServer> { return new Promise((resolve) => { const ac = new AbortController(); const server = Deno.serve({ handler, port: 0, signal: ac.signal, onListen({ port }) { logger.debug("Temporary server is listening on port {port}.", { port }); openTunnel({ port }).then((tun) => { logger.debug( "Temporary server is tunneled to {url}.", { url: tun.url.href }, ); resolve({ url: tun.url, async close() { await server.shutdown(); await tun.close(); }, }); }); }, }); }); }
deno.json +4 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ "imports": { "@cfworker/json-schema": "npm:@cfworker/json-schema@^1.12.8", "@cliffy/command": "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts", "@cliffy/prompt": "https://deno.land/x/cliffy@v1.0.0-rc.4/prompt/mod.ts", "@david/which-runtime": "jsr:@david/which-runtime@^0.2.0", "@deno/dnt": "jsr:@deno/dnt@^0.41.1", "@fedify/fedify": "./mod.ts", Loading @@ -29,6 +30,7 @@ "@fedify/fedify/x/fresh": "./x/fresh.ts", "@fedify/fedify/x/hono": "./x/hono.ts", "@hongminhee/aitertools": "jsr:@hongminhee/aitertools@^0.6.0", "@hongminhee/localtunnel": "jsr:@hongminhee/localtunnel@^0.1.0", "@logtape/logtape": "jsr:@logtape/logtape@^0.2.2", "@phensley/language-tag": "npm:@phensley/language-tag@^1.8.0", "@std/assert": "jsr:@std/assert@^0.220.1", Loading @@ -51,6 +53,7 @@ "fast-check": "npm:fast-check@^3.17.0", "jsonld": "npm:jsonld@^8.3.2", "mock_fetch": "https://deno.land/x/mock_fetch@0.3.0/mod.ts", "ora": "npm:ora@^8.0.1", "uri-template-router": "npm:uri-template-router@^0.0.16", "url-template": "npm:url-template@^3.1.1" }, Loading @@ -68,7 +71,7 @@ "cache": "deno task codegen && deno cache mod.ts", "check": "deno task codegen && deno fmt --check && deno lint && deno check */*.ts", "codegen": "deno run --allow-read --allow-write --check codegen/main.ts vocab/ ../runtime/ > vocab/vocab.ts && deno fmt vocab/vocab.ts && deno cache vocab/vocab.ts && deno check vocab/vocab.ts", "cli": "deno task codegen && deno run --allow-read --allow-net --allow-env cli/mod.ts", "cli": "deno task codegen && deno run --allow-read --allow-net --allow-env --allow-run cli/mod.ts", "test-without-codegen": "deno test --check --doc --allow-read --allow-write --unstable-kv --trace-leaks", "test": "deno task codegen && deno task test-without-codegen", "coverage": "rm -rf coverage/ && deno task test --coverage && deno coverage --html coverage", Loading