···11+import { encode as cborEncode } from "@atcute/cbor";
12import type { ComAtprotoLabelDefs } from "@atproto/api";
22-import { ErrorFrame, MessageFrame } from "@atproto/xrpc-server";
33+import { Keypair, Secp256k1Keypair } from "@atproto/crypto";
44+import { ErrorFrame, InvalidRequestError, MessageFrame, XRPCError } from "@atproto/xrpc-server";
35import Database, { type Database as SQLiteDatabase } from "better-sqlite3";
46import express from "express";
57import expressWs, { type Application, WebsocketRequestHandler } from "express-ws";
68import { Server } from "node:http";
99+import { fromString as ui8FromString } from "uint8arrays";
710import type { WebSocket } from "ws";
1111+import { SignedLabel, StrictPartial } from "./util.js";
1212+1313+const LABEL_VERSION = 1;
814915export interface LabelerOptions {
1016 did: string;
1717+ signingKey: string;
1118 dbFile?: string;
1219}
1320···1926 db: SQLiteDatabase;
20272128 did: string;
2929+3030+ private signingKey: Keypair;
22312332 private subscriptions = new Set<WebSocket>();
24332534 constructor(options: LabelerOptions) {
2635 this.did = options.did;
3636+ this.signingKey = new Secp256k1Keypair(ui8FromString(options.signingKey, "hex"), false);
27372838 this.db = new Database(options.dbFile ?? "labels.db");
2939 this.db.pragma("journal_mode = WAL");
···4252 `);
43534454 this.app = expressWs(express().use(express.json())).app;
5555+ this.app.get("/xrpc/com.atproto.label.queryLabels", this.queryLabelsHandler);
4556 this.app.ws("/xrpc/com.atproto.label.subscribeLabels", this.subscribeLabelsHandler);
4657 }
4758···5364 if (this.server?.listening) this.server?.close(callback);
5465 }
55665656- subscribeLabelsHandler: WebsocketRequestHandler = async (ws, req) => {
6767+ async signLabel(label: ComAtprotoLabelDefs.Label): Promise<SignedLabel> {
6868+ const toSign = this.formatLabel(label);
6969+ const bytes = cborEncode(toSign);
7070+ const sig = await this.signingKey.sign(bytes);
7171+ return { ...toSign, sig };
7272+ }
7373+7474+ private formatLabel<T extends ComAtprotoLabelDefs.Label>(label: T): StrictPartial<T> {
7575+ const { src, uri, cid, val, neg, cts, exp } = label;
7676+ return {
7777+ ver: LABEL_VERSION,
7878+ src,
7979+ uri,
8080+ ...(cid ? { cid } : {}),
8181+ val,
8282+ ...(neg ? { neg } : {}),
8383+ cts,
8484+ ...(exp ? { exp } : {}),
8585+ } as never;
8686+ }
8787+8888+ private async ensureSignedLabel(label: ComAtprotoLabelDefs.Label): Promise<SignedLabel> {
8989+ if (!label.sig) {
9090+ const signed = await this.signLabel(label);
9191+ const stmt = this.db.prepare(`
9292+ UPDATE labels
9393+ SET sig = ?
9494+ WHERE id = ?
9595+ `).run(signed.sig, label.id);
9696+ if (!stmt.changes) throw new Error("Failed to update label with signature");
9797+ return signed;
9898+ }
9999+ return this.formatLabel(label) as ComAtprotoLabelDefs.Label & { sig: Uint8Array };
100100+ }
101101+102102+ queryLabelsHandler: express.RequestHandler = async (req, res) => {
103103+ try {
104104+ const { uriPatterns, sources, limit: limitStr, cursor: cursorStr } = req.query as {
105105+ uriPatterns?: Array<string>;
106106+ sources?: Array<string>;
107107+ limit?: string;
108108+ cursor?: string;
109109+ };
110110+111111+ const cursor = cursorStr ? parseInt(cursorStr, 10) : undefined;
112112+ if (cursor && Number.isNaN(cursor)) {
113113+ throw new InvalidRequestError("Cursor must be an integer");
114114+ }
115115+116116+ const limit = parseInt(limitStr ?? "50", 10);
117117+ if (Number.isNaN(limit) || limit < 1 || limit > 250) {
118118+ throw new InvalidRequestError("Limit must be an integer between 1 and 250");
119119+ }
120120+121121+ const patterns = uriPatterns?.includes("*")
122122+ ? undefined
123123+ : uriPatterns?.map((pattern) => {
124124+ if (pattern.indexOf("*") !== pattern.length - 1) {
125125+ throw new InvalidRequestError(
126126+ "Only trailing wildcards are supported in uriPatterns",
127127+ );
128128+ }
129129+ return pattern.replaceAll(/%/g, "").replaceAll(/_/g, "\\_").slice(0, -1) + "%";
130130+ });
131131+132132+ const stmt = this.db.prepare<unknown[], ComAtprotoLabelDefs.Label>(`
133133+ SELECT * FROM labels
134134+ ${patterns?.length ? patterns.map(() => "WHERE uri LIKE ?").join(" OR ") : ""}
135135+ ${sources?.length ? "AND src IN (?)" : ""}
136136+ ${cursor ? "AND id > ?" : ""}
137137+ ORDER BY id ASC
138138+ LIMIT ?
139139+ `);
140140+141141+ const rows = stmt.all([...(patterns ?? []), sources ?? [], cursor ?? 0, limit]);
142142+143143+ const labels = await Promise.all(rows.map((row) => this.ensureSignedLabel(row)));
144144+ const nextCursor = rows[rows.length - 1]?.id ?? 0;
145145+146146+ res.json({ cursor: nextCursor, labels });
147147+ } catch (e) {
148148+ if (e instanceof XRPCError) {
149149+ res.status(e.type).json(e.payload);
150150+ } else {
151151+ console.error(e);
152152+ res.status(500).json({
153153+ error: "InternalError",
154154+ message: e instanceof Error ? e.message : "An unknown error occurred",
155155+ });
156156+ }
157157+ return;
158158+ }
159159+ };
160160+161161+ subscribeLabelsHandler: WebsocketRequestHandler = (ws, req) => {
57162 const cursor = parseInt(req.params.cursor);
5816359164 if (cursor && !Number.isNaN(cursor)) {
+7
src/util.ts
···11+import type { ComAtprotoLabelDefs } from "@atproto/api";
22+33+export type StrictPartial<T> =
44+ & { [K in keyof T as undefined extends T[K] ? never : K]: T[K] }
55+ & { [K in keyof T as undefined extends T[K] ? K : never]?: T[K] };
66+77+export type SignedLabel = ComAtprotoLabelDefs.Label & { sig: Uint8Array };