From 7322ea3d6aa3049c5790fdc879161f0ee8b79e5f Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Sat, 8 Nov 2025 21:11:08 +0100 Subject: [PATCH 1/3] Use handle resolver from atcute instead of custom resolver --- src/bin.ts | 95 ++++++++++++++++++++++++++------------- src/util/resolveHandle.ts | 50 --------------------- src/util/resolver.ts | 20 +++++++++ 3 files changed, 83 insertions(+), 82 deletions(-) delete mode 100644 src/util/resolveHandle.ts create mode 100644 src/util/resolver.ts diff --git a/src/bin.ts b/src/bin.ts index bf09611..03c96fe 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,5 +1,7 @@ #!/usr/bin/env node import type { ComAtprotoLabelDefs } from "@atcute/atproto"; +import { getPdsEndpoint, isAtprotoDid } from "@atcute/identity"; +import { AtprotoDid } from "@atcute/lexicons/syntax"; import { XRPCError } from "@atcute/xrpc-server"; import { spawn } from "node:child_process"; import * as fs from "node:fs/promises"; @@ -17,21 +19,58 @@ import { setLabelerLabelDefinitions, } from "./scripts/index.js"; import { loginAgent } from "./scripts/util.js"; -import { resolveHandle } from "./util/resolveHandle.js"; +import { didResolver, handleResolver } from "./util/resolver.js"; const argv = process.argv.slice(2); const [command, subcommand, ...args] = argv; -if (command === "setup" || command === "clear") { - const credentials = await promptCredentials(); +const abortController = new AbortController(); +abortController.signal.addEventListener("abort", () => { + process.exit(1); +}); + +const onCancel = () => { + abortController.abort(); +}; + +if (!["setup", "clear", "recreate", "label"].includes(command)) { + console.log("Usage: npx @skyware/labeler [command]"); + console.log("Commands:"); + console.log(" setup - Initialize an account as a labeler."); + console.log(" clear - Restore a labeler account to normal."); + console.log( + " recreate - Recreate the labeler declaration (recommended if labels are not showing up).", + ); + console.log(" label add - Add new label declarations to a labeler account."); + console.log(" label delete - Remove label declarations from a labeler account."); + console.log(" label edit - Bulk edit label definitions."); + + process.exit(0); +} + +const credentials = await promptCredentials().catch((err) => { + console.error(err); + process.exit(1); +}); + +// Since these commands rely on PLC operations, we can't proceed with a did:web identifier: +if ( + ["setup", "clear", "recreate"].includes(command) && credentials.identifier.startsWith("did:web") +) { + console.error( + `npx @skyware/labeler currently only supports did:plc identities for ${command}, received: ${credentials.identifier}`, + ); + process.exit(1); +} +if (command === "setup" || command === "clear") { await plcRequestToken(credentials); const { plcToken } = await prompt({ type: "text", name: "plcToken", message: "You will receive a confirmation code via email. Code:", - }, { onCancel: () => process.exit(1) }); + }, { onCancel }); if (command === "setup") { try { @@ -51,7 +90,7 @@ if (command === "setup" || command === "clear") { if (/^[A-Za-z0-9+/=]+$/.test(value)) return true; return "Must be a hex or base64-encoded string."; }, - }], { onCancel: () => process.exit(1) }); + }], { onCancel }); const operation = await plcSetupLabeler({ ...credentials, @@ -95,8 +134,6 @@ if (command === "setup" || command === "clear") { } } } else if (command === "recreate") { - const credentials = await promptCredentials(); - const definitions = await getLabelerLabelDefinitions(credentials); if (!definitions) { console.log("No label definitions found."); @@ -114,7 +151,6 @@ if (command === "setup" || command === "clear") { command === "label" && (subcommand === "add" || subcommand === "delete" || subcommand === "edit") ) { - const credentials = await promptCredentials(); const labelDefinitions = await getLabelerLabelDefinitions(credentials) ?? []; if (subcommand === "add") { @@ -191,21 +227,10 @@ if (command === "setup" || command === "clear") { console.error("Error updating label definitions:", error); } } -} else { - console.log("Usage: npx @skyware/labeler [command]"); - console.log("Commands:"); - console.log(" setup - Initialize an account as a labeler."); - console.log(" clear - Restore a labeler account to normal."); - console.log( - " recreate - Recreate the labeler declaration (recommended if labels are not showing up).", - ); - console.log(" label add - Add new label declarations to a labeler account."); - console.log(" label delete - Remove label declarations from a labeler account."); - console.log(" label edit - Bulk edit label definitions."); } async function promptCredentials(): Promise { - let did: string | undefined; + let did: AtprotoDid | undefined; while (!did) { const { did: didOrHandle } = await prompt({ type: "text", @@ -214,25 +239,31 @@ async function promptCredentials(): Promise { validate: (value) => value.startsWith("did:") || value.includes(".") || "Invalid DID or handle.", format: (value) => value.startsWith("@") ? value.slice(1) : value, - }, { onCancel: () => process.exit(1) }); + }, { onCancel }); if (!didOrHandle) continue; - did = didOrHandle.startsWith("did:") ? didOrHandle : await resolveHandle(didOrHandle); + + did = isAtprotoDid(didOrHandle) ? didOrHandle : await handleResolver.resolve(didOrHandle); + if (!did) { - console.log(`Could not resolve "${didOrHandle}" to a valid account. Please try again.`); + throw new Error( + `Could not resolve "${didOrHandle}" to a valid account. Please try again.`, + ); } } - const { password, pds } = await prompt([{ + const didDoc = await didResolver.resolve(did); + const pds = getPdsEndpoint(didDoc); + + // This shouldn't really ever happen in practice, + if (!pds) { + throw new Error(`Could not resolve "${did}" to a valid pds.`); + } + + const { password } = await prompt([{ type: "password", name: "password", message: "Account password (cannot be an app password):", - }, { - type: "text", - name: "pds", - message: "URL of the PDS where the account is located:", - initial: "https://bsky.social", - validate: (value) => value.startsWith("https://") || "Must be a valid HTTPS URL.", - }], { onCancel: () => process.exit(1) }); + }], { onCancel }); const credentials: LoginCredentials = { identifier: did, password, pds }; @@ -245,7 +276,7 @@ async function promptCredentials(): Promise { name: "code", message: "You will receive a 2FA code via email. Code:", initial: "", - }, { onCancel: () => process.exit(1) }); + }, { onCancel }); credentials.code = code; } else { console.error("Error occurred while trying to log in:", error); diff --git a/src/util/resolveHandle.ts b/src/util/resolveHandle.ts deleted file mode 100644 index 526d7ae..0000000 --- a/src/util/resolveHandle.ts +++ /dev/null @@ -1,50 +0,0 @@ -import dns from "dns/promises"; - -export async function resolveHandle(handle: string): Promise { - const dnsPromise = resolveDns(handle); - const httpAbort = new AbortController(); - const httpPromise = resolveHttp(handle, httpAbort.signal).catch(() => undefined); - - const dnsRes = await dnsPromise; - if (dnsRes) { - httpAbort.abort(); - return dnsRes; - } - const res = await httpPromise; - if (res) { - return res; - } -} - -async function resolveDns(handle: string): Promise { - let chunkedResults: string[][]; - try { - chunkedResults = await dns.resolveTxt(`_atproto.${handle}`); - } catch (err) { - return undefined; - } - return parseDnsResult(chunkedResults); -} - -async function resolveHttp(handle: string, signal?: AbortSignal): Promise { - const url = new URL("/.well-known/atproto-did", `https://${handle}`); - try { - const res = await fetch(url, signal ? { signal } : undefined); - const did = (await res.text()).split("\n")[0].trim(); - if (typeof did === "string" && did.startsWith("did:")) { - return did; - } - return undefined; - } catch (err) { - return undefined; - } -} - -function parseDnsResult(chunkedResults: string[][]): string | undefined { - const results = chunkedResults.map((chunks) => chunks.join("")); - const found = results.filter((i) => i.startsWith("did=")); - if (found.length !== 1) { - return undefined; - } - return found[0].slice("did=".length); -} diff --git a/src/util/resolver.ts b/src/util/resolver.ts new file mode 100644 index 0000000..283c428 --- /dev/null +++ b/src/util/resolver.ts @@ -0,0 +1,20 @@ +import { + CompositeDidDocumentResolver, + CompositeHandleResolver, + DohJsonHandleResolver, + PlcDidDocumentResolver, + WebDidDocumentResolver, + WellKnownHandleResolver, +} from "@atcute/identity-resolver"; + +export const handleResolver = new CompositeHandleResolver({ + strategy: "race", + methods: { + dns: new DohJsonHandleResolver({ dohUrl: "https://mozilla.cloudflare-dns.com/dns-query" }), + http: new WellKnownHandleResolver(), + }, +}); + +export const didResolver = new CompositeDidDocumentResolver({ + methods: { plc: new PlcDidDocumentResolver(), web: new WebDidDocumentResolver() }, +}); From 9858da8181338a0e203f74c6d176a1b66ee95f6a Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Sat, 8 Nov 2025 21:14:29 +0100 Subject: [PATCH 2/3] Fix type error when running CLI --- src/scripts/declareLabeler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/declareLabeler.ts b/src/scripts/declareLabeler.ts index e990ea4..ec66673 100644 --- a/src/scripts/declareLabeler.ts +++ b/src/scripts/declareLabeler.ts @@ -1,4 +1,4 @@ -import "@atcute/bluesky/lexicons"; +import type {} from "@atcute/bluesky"; import { ComAtprotoLabelDefs, ComAtprotoRepoPutRecord } from "@atcute/atproto"; import { AppBskyLabelerService } from "@atcute/bluesky"; import { is } from "@atcute/lexicons"; From 8a91edf6849926065b82d362265847bbf9cbfc4f Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Sat, 8 Nov 2025 21:34:34 +0100 Subject: [PATCH 3/3] Use correct format for com.atproto.identity.requestPlcOperationSignature --- src/scripts/plc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/plc.ts b/src/scripts/plc.ts index 4312612..1a2e760 100644 --- a/src/scripts/plc.ts +++ b/src/scripts/plc.ts @@ -178,5 +178,5 @@ export async function plcClearLabeler(options: PlcClearLabelerOptions) { */ export async function plcRequestToken(credentials: LoginCredentials): Promise { const { agent } = await loginAgent(credentials); - await agent.post("com.atproto.identity.requestPlcOperationSignature", { as: "json" }); + await agent.post("com.atproto.identity.requestPlcOperationSignature", { as: null }); }