···11+import { AtpAgent, ComAtprotoIdentitySignPlcOperation } from "@atproto/api";
22+import { P256Keypair, Secp256k1Keypair } from "@atproto/crypto";
33+import * as ui8 from "uint8arrays";
44+55+/** Options for the {@link plcSetupLabeler} function. */
66+export interface PlcSetupLabelerOptions {
77+ /** The HTTPS URL where the labeler is hosted. */
88+ endpoint: string;
99+1010+ /**
1111+ * The token to use to sign the PLC operation.
1212+ * If you don't have a token, first call {@link plcRequestToken} to receive one via email.
1313+ */
1414+ plcToken: string;
1515+1616+ /** The URL of the PDS where the labeler account is located, if different from bsky.social. */
1717+ pds?: string;
1818+ /** The DID of the labeler account. */
1919+ did: string;
2020+ /** The password of the labeler account. You must provide either `password` or `agent`. */
2121+ password?: string;
2222+ /** An agent logged into the labeler account. You must provide either `password` or `agent`. */
2323+ agent?: AtpAgent;
2424+2525+ /** You may choose to provide your own signing key to use for the labeler. */
2626+ privateKey?: string | Uint8Array;
2727+ /** The algorithm of the provided private key. */
2828+ privateKeyAlgorithm?: "secp256k1" | "secp256r1";
2929+ /** Whether to overwrite the existing label signing key if one is already set. */
3030+ overwriteExistingKey?: boolean;
3131+}
3232+3333+/**
3434+ * This function will update the labeler account's DID document to include the
3535+ * provided labeler endpoint and signing key. If no private key is provided, a
3636+ * new keypair will be generated, and the private key will be printed to the
3737+ * console. This private key will be needed to sign any labels created.
3838+ * @param options Options for the function.
3939+ */
4040+export async function plcSetupLabeler(options: PlcSetupLabelerOptions) {
4141+ if (!options.agent && !options.password) {
4242+ throw new Error(
4343+ "Either a logged-in agent or a password must be provided for the labeler account.",
4444+ );
4545+ }
4646+4747+ const agent = options.agent ?? new AtpAgent({ service: options.pds || "https://bsky.social" });
4848+ if (!agent.hasSession) {
4949+ if (!options.password) {
5050+ throw new Error("A password must be provided to log in to the labeler account.");
5151+ }
5252+ await agent.login({ identifier: options.did, password: options.password });
5353+ }
5454+5555+ let keypair: Secp256k1Keypair | P256Keypair;
5656+ if (options.privateKey) {
5757+ if (options.privateKeyAlgorithm === "secp256r1") {
5858+ keypair = await P256Keypair.import(options.privateKey);
5959+ } else if (options.privateKeyAlgorithm === "secp256k1") {
6060+ keypair = await Secp256k1Keypair.import(options.privateKey);
6161+ } else {
6262+ throw new Error("Invalid private key algorithm.");
6363+ }
6464+ } else {
6565+ keypair = await Secp256k1Keypair.create({ exportable: true });
6666+ }
6767+6868+ const keyDid = keypair.did();
6969+7070+ const operation: ComAtprotoIdentitySignPlcOperation.InputSchema = {};
7171+7272+ const credentials = await agent.com.atproto.identity.getRecommendedDidCredentials();
7373+ if (!credentials.success) {
7474+ throw new Error("Failed to fetch DID document.");
7575+ }
7676+7777+ if (
7878+ !credentials.data.verificationMethods
7979+ || !("atproto_label" in credentials.data.verificationMethods)
8080+ || !credentials.data.verificationMethods["atproto_label"]
8181+ || (credentials.data.verificationMethods["atproto_label"] !== keyDid
8282+ && options.overwriteExistingKey)
8383+ ) {
8484+ operation.verificationMethods = {
8585+ ...(credentials.data.verificationMethods || {}),
8686+ atproto_label: keyDid,
8787+ };
8888+ }
8989+9090+ if (
9191+ !credentials.data.services
9292+ || !("atproto_label" in credentials.data.services)
9393+ || !credentials.data.services["atproto_label"]
9494+ || typeof credentials.data.services["atproto_label"] !== "object"
9595+ || !("endpoint" in credentials.data.services["atproto_label"])
9696+ || credentials.data.services["atproto_label"].endpoint !== options.endpoint
9797+ ) {
9898+ operation.services = {
9999+ ...(credentials.data.services || {}),
100100+ atproto_label: { type: "AtprotoLabeler", endpoint: options.endpoint },
101101+ };
102102+ }
103103+104104+ if (Object.keys(operation).length === 0) {
105105+ return;
106106+ }
107107+108108+ const plcOp = await agent.com.atproto.identity.signPlcOperation({
109109+ token: options.plcToken,
110110+ ...operation,
111111+ });
112112+113113+ await agent.com.atproto.identity.submitPlcOperation({ operation: plcOp.data.operation });
114114+115115+ if (!options.privateKey && operation.verificationMethods) {
116116+ const privateKey = ui8.toString(await keypair.export(), "hex");
117117+ console.log(
118118+ "This is your labeler's signing key. It will be needed to sign any labels you create.",
119119+ "You will not be able to retrieve this key again, so make sure to save it somewhere safe.",
120120+ "If you lose this key, you can call this function again without passing a private key to generate a new one.",
121121+ );
122122+ console.log("Signing key:", privateKey);
123123+ }
124124+}
125125+126126+/**
127127+ * Request a PLC token, needed for {@link plcSetupLabeler}. The token will be sent to the email
128128+ * associated with the labeler account.
129129+ * @param agent An agent logged into the labeler account.
130130+ */
131131+export async function plcRequestToken(agent: AtpAgent): Promise<void>;
132132+/**
133133+ * Request a PLC token, needed for {@link plcSetupLabeler}. The token will be sent to the email
134134+ * associated with the labeler account.
135135+ * @param credentials The credentials of the labeler account.
136136+ */
137137+export async function plcRequestToken(
138138+ credentials: { pds?: string; identifier: string; password: string },
139139+): Promise<void>;
140140+export async function plcRequestToken(
141141+ agentOrCredentials: AtpAgent | { pds?: string; identifier: string; password: string },
142142+) {
143143+ const agent = agentOrCredentials instanceof AtpAgent
144144+ ? agentOrCredentials
145145+ : new AtpAgent({ service: agentOrCredentials.pds || "https://bsky.social" });
146146+ if (!agent.hasSession) {
147147+ if (!(agentOrCredentials instanceof AtpAgent)) {
148148+ await agent.login(agentOrCredentials);
149149+ } else {
150150+ throw new Error("A password must be provided to log in to the labeler account.");
151151+ }
152152+ }
153153+ await agent.com.atproto.identity.requestPlcOperationSignature();
154154+}