Skip to content
This repository was archived by the owner on Feb 18, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 63 additions & 32 deletions src/bin.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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.");
Expand All @@ -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") {
Expand Down Expand Up @@ -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<LoginCredentials> {
let did: string | undefined;
let did: AtprotoDid | undefined;
while (!did) {
const { did: didOrHandle } = await prompt({
type: "text",
Expand All @@ -214,25 +239,31 @@ async function promptCredentials(): Promise<LoginCredentials> {
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 };

Expand All @@ -245,7 +276,7 @@ async function promptCredentials(): Promise<LoginCredentials> {
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);
Expand Down
2 changes: 1 addition & 1 deletion src/scripts/declareLabeler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import "@atcute/bluesky/lexicons";
import type {} from "@atcute/bluesky";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug introduced in #14 — the types for the @atcute/client were incorrectly imported here.

import { ComAtprotoLabelDefs, ComAtprotoRepoPutRecord } from "@atcute/atproto";
import { AppBskyLabelerService } from "@atcute/bluesky";
import { is } from "@atcute/lexicons";
Expand Down
2 changes: 1 addition & 1 deletion src/scripts/plc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,5 +178,5 @@ export async function plcClearLabeler(options: PlcClearLabelerOptions) {
*/
export async function plcRequestToken(credentials: LoginCredentials): Promise<void> {
const { agent } = await loginAgent(credentials);
await agent.post("com.atproto.identity.requestPlcOperationSignature", { as: "json" });
await agent.post("com.atproto.identity.requestPlcOperationSignature", { as: null });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug introduced in #14 — the response type for this method is null.

}
50 changes: 0 additions & 50 deletions src/util/resolveHandle.ts

This file was deleted.

20 changes: 20 additions & 0 deletions src/util/resolver.ts
Original file line number Diff line number Diff line change
@@ -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() },
});